Files
kubevela/test/e2e-test/helmchart_test.go
Ayush Kumar 28892ccc7e Feat: implement valuesFrom support for helmchart component and update… (#7099)
* feat: implement valuesFrom support for helmchart component and update documentation examples

Signed-off-by: Anaswara Suresh <anaswarasuresh2212@gmail.com>

* fix: address cubic review feedback on valuesFrom

Three issues raised by cubic AI review on kubevela#7099:

1. docs/examples/helmchart-valuesfrom/secret-and-inline.yaml —
   Expected-result comments used incorrect paths (resources.cpu /
   resources.mem) and values (500m) that did not match the actual CM
   data. Rewrote the narrative to use the real paths
   (resources.limits.cpu / resources.limits.memory) and bundled the
   Secret inline in the manifest so the example is self-contained and
   the expected output is deterministic.

2. docs/examples/helmchart-valuesfrom/secret-and-inline.yaml —
   The Secret was marked optional: true while the narrative required
   it for the merged output to match. Bundled the Secret inline and
   dropped the optional flag, removing the order/timing ambiguity.
   README.md updated to drop the now-redundant "create the Secret
   first" instruction.

3. pkg/cue/cuex/providers/helm/helm.go:Render —
   After removing the unconditional "default" fallback for
   releaseNamespace, Helm could run with an empty namespace when both
   Context.AppNamespace and Release.Namespace were unset (non-normal
   code paths — direct callers, tests, CLI tooling). Restored the
   "default" fallback at the end of the namespace resolution while
   keeping the Application-namespace plumbing for tenant-scoped
   cross-namespace rejection.

Co-authored-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Anaswara Suresh <anaswarasuresh2212@gmail.com>

* feat: add valuesFrom fingerprinting for helmchart components to trigger workflow restarts on ConfigMap/Secret changes

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: add valuesFrom fingerprinting for helmchart components to trigger workflow restarts on ConfigMap/Secret changes

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: enhance valuesFrom support for helmchart components with fingerprinting and error handling

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* docs: clarify cross-namespace restrictions for valuesFrom in helmchart examples

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* ci: retrigger checks

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: add publishVersion support to Helm provider for stable release management

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: improve error handling for application retrieval in Helm provider

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* ci: retrigger checks

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: add application publishVersion lookup defense in Helm provider tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

---------

Signed-off-by: Anaswara Suresh <anaswarasuresh2212@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Anaswara Suresh <anaswarasuresh2212@gmail.com>
2026-04-28 20:02:58 -07:00

2086 lines
79 KiB
Go

/*
Copyright 2025 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/kubevela/pkg/util/rand"
common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
type helmTestContext struct {
ctx context.Context
namespace string
appNamespace string
app *v1beta1.Application
appKey client.ObjectKey
}
func newHelmTestContext() *helmTestContext {
return &helmTestContext{
ctx: context.Background(),
namespace: "helm-e2e-" + rand.RandomString(4),
appNamespace: "default",
}
}
func (h *helmTestContext) createNamespace() {
By("Creating target namespace for Helm release: " + h.namespace)
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: h.namespace}}
Expect(k8sClient.Create(h.ctx, ns)).Should(SatisfyAny(Succeed(), Not(HaveOccurred())))
}
func (h *helmTestContext) cleanup() {
By("Deleting Application if it exists")
if h.app != nil {
_ = k8sClient.Delete(h.ctx, h.app)
Eventually(func() bool {
err := k8sClient.Get(h.ctx, h.appKey, &v1beta1.Application{})
return err != nil
}, 60*time.Second, 2*time.Second).Should(BeTrue())
}
By("Deleting target namespace")
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: h.namespace}}
_ = k8sClient.Delete(h.ctx, ns, client.PropagationPolicy(metav1.DeletePropagationForeground))
}
func (h *helmTestContext) cleanupNamespaceOnly() {
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: h.namespace}}
_ = k8sClient.Delete(h.ctx, ns, client.PropagationPolicy(metav1.DeletePropagationForeground))
}
func (h *helmTestContext) deployApp() {
h.deployAppFrom("testdata/helm/app_helmchart_podinfo.yaml")
}
func (h *helmTestContext) deployAppFrom(yamlPath string) {
By("Deploying helmchart Application from " + yamlPath)
raw, err := os.ReadFile(yamlPath)
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("podinfo-helm-test-" + rand.RandomString(4))
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
By("Waiting for Application to reach running state")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 120*time.Second, 3*time.Second).Should(Succeed())
}
func (h *helmTestContext) waitForDeploymentReady() {
By("Waiting for Deployment to be ready with 2 replicas")
Eventually(func(g Gomega) {
deploy := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{
Namespace: h.namespace, Name: "podinfo",
}, deploy)).Should(Succeed())
g.Expect(deploy.Status.ReadyReplicas).Should(Equal(int32(2)))
}, 120*time.Second, 3*time.Second).Should(Succeed())
}
func (h *helmTestContext) getHelmSecrets() *corev1.SecretList {
secrets := &corev1.SecretList{}
Expect(k8sClient.List(h.ctx, secrets,
client.InNamespace(h.namespace),
client.MatchingLabels{"owner": "helm", "name": "podinfo"},
)).Should(Succeed())
return secrets
}
func (h *helmTestContext) waitForAppRunning() {
By("Waiting for Application to return to running state")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 180*time.Second, 3*time.Second).Should(Succeed())
}
func (h *helmTestContext) latestHelmSecretName() string {
secrets := h.getHelmSecrets()
var latest string
for _, s := range secrets.Items {
if latest == "" || s.Name > latest {
latest = s.Name
}
}
return latest
}
func (h *helmTestContext) updateAppValues(values map[string]interface{}) {
By("Updating Application values to trigger upgrade")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
raw, err := json.Marshal(h.app.Spec.Components[0].Properties)
g.Expect(err).Should(BeNil())
var props map[string]interface{}
g.Expect(json.Unmarshal(raw, &props)).Should(BeNil())
if existing, ok := props["values"].(map[string]interface{}); ok {
for k, v := range values {
existing[k] = v
}
props["values"] = existing
} else {
props["values"] = values
}
newRaw, err := json.Marshal(props)
g.Expect(err).Should(BeNil())
h.app.Spec.Components[0].Properties = &runtime.RawExtension{Raw: newRaw}
g.Expect(k8sClient.Update(h.ctx, h.app)).Should(Succeed())
}, 30*time.Second, time.Second).Should(Succeed())
h.waitForAppRunning()
}
func (h *helmTestContext) recordPodUIDs() map[types.UID]bool {
podList := &corev1.PodList{}
Expect(k8sClient.List(h.ctx, podList,
client.InNamespace(h.namespace),
client.MatchingLabels{"app.kubernetes.io/name": "podinfo"},
)).Should(Succeed())
uids := make(map[types.UID]bool)
for _, pod := range podList.Items {
uids[pod.UID] = true
}
return uids
}
func (h *helmTestContext) countSurvivingPods(originalUIDs map[types.UID]bool) int {
podList := &corev1.PodList{}
Expect(k8sClient.List(h.ctx, podList,
client.InNamespace(h.namespace),
client.MatchingLabels{"app.kubernetes.io/name": "podinfo"},
)).Should(Succeed())
count := 0
for _, pod := range podList.Items {
if originalUIDs[pod.UID] {
count++
}
}
return count
}
func runCommand(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
out, err := cmd.CombinedOutput()
GinkgoWriter.Printf("$ %s %v\n%s\n", name, args, string(out))
return string(out), err
}
func runCommandSucceed(name string, args ...string) string {
out, err := runCommand(name, args...)
Expect(err).Should(Succeed(), "command failed: %s %v\noutput: %s", name, args, out)
return out
}
// ============================================================================
// Self-Healing Scenarios
// ============================================================================
var _ = Describe("Helmchart Self-Healing", func() {
Context("Delete a Single Managed Resource (Deployment)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo successfully", func() {
h.deployApp()
h.waitForDeploymentReady()
By("Verifying Helm release secret exists")
Expect(len(h.getHelmSecrets().Items)).Should(BeNumerically(">=", 1))
})
It("should recover when the Deployment is deleted via kubectl", func() {
initialCount := len(h.getHelmSecrets().Items)
latestSecret := h.latestHelmSecretName()
By("Deleting the Deployment via kubectl")
runCommandSucceed("kubectl", "delete", "deployment", "podinfo", "-n", h.namespace)
By("Verifying Deployment is gone")
Eventually(func() bool {
err := k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, &appsv1.Deployment{})
return err != nil
}, 10*time.Second, time.Second).Should(BeTrue())
By("Triggering reconciliation")
RequestReconcileNow(h.ctx, h.app)
By("Verifying KubeVela recreates the Deployment")
h.waitForDeploymentReady()
By("Verifying Helm revision did NOT increment (recovery is via ResourceTracker)")
Expect(len(h.getHelmSecrets().Items)).Should(Equal(initialCount))
Expect(h.latestHelmSecretName()).Should(Equal(latestSecret))
By("Verifying pods are back to desired replica count")
deploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
Expect(*deploy.Spec.Replicas).Should(Equal(int32(2)))
})
})
Context("Helm Uninstall the Release", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo successfully", func() {
h.deployApp()
h.waitForDeploymentReady()
out := runCommandSucceed("helm", "list", "-n", h.namespace, "-q")
Expect(out).Should(ContainSubstring("podinfo"))
})
It("should recover after external helm uninstall", func() {
By("Running helm uninstall podinfo externally")
runCommandSucceed("helm", "uninstall", "podinfo", "-n", h.namespace)
By("Verifying helm list no longer shows the release")
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", h.namespace, "-q")
return out
}, 15*time.Second, time.Second).ShouldNot(ContainSubstring("podinfo"))
By("Verifying Deployment is gone after uninstall")
Eventually(func() bool {
err := k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, &appsv1.Deployment{})
return err != nil
}, 30*time.Second, 2*time.Second).Should(BeTrue())
By("Triggering reconciliation")
RequestReconcileNow(h.ctx, h.app)
By("Verifying KubeVela performs fresh helm install")
h.waitForDeploymentReady()
By("Verifying helm list shows the release again")
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", h.namespace, "-q")
return out
}, 60*time.Second, 3*time.Second).Should(ContainSubstring("podinfo"))
h.waitForAppRunning()
})
})
Context("Delete ONLY the Helm Release Secret", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo successfully", func() {
h.deployApp()
h.waitForDeploymentReady()
})
It("should recover release secrets without affecting running pods", func() {
originalPodUIDs := h.recordPodUIDs()
Expect(len(originalPodUIDs)).Should(BeNumerically(">=", 2))
By("Deleting all Helm release secrets via kubectl")
runCommandSucceed("kubectl", "delete", "secrets", "-l", "owner=helm,name=podinfo", "-n", h.namespace)
By("Verifying Helm release secrets are gone")
Eventually(func() int {
return len(h.getHelmSecrets().Items)
}, 15*time.Second, time.Second).Should(Equal(0))
By("Verifying running pods are NOT affected")
Consistently(func() int {
pods := &corev1.PodList{}
Expect(k8sClient.List(h.ctx, pods, client.InNamespace(h.namespace),
client.MatchingLabels{"app.kubernetes.io/name": "podinfo"})).Should(Succeed())
count := 0
for _, pod := range pods.Items {
if pod.Status.Phase == corev1.PodRunning {
count++
}
}
return count
}, 10*time.Second, 2*time.Second).Should(BeNumerically(">=", 2))
By("Triggering reconciliation")
RequestReconcileNow(h.ctx, h.app)
By("Verifying KubeVela restores Helm release secrets")
Eventually(func() int {
return len(h.getHelmSecrets().Items)
}, 120*time.Second, 3*time.Second).Should(BeNumerically(">=", 1))
By("Verifying helm list shows the release again")
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", h.namespace, "-q")
return out
}, 60*time.Second, 3*time.Second).Should(ContainSubstring("podinfo"))
By("Verifying original pods still exist (not restarted)")
Expect(h.countSurvivingPods(originalPodUIDs)).Should(BeNumerically(">=", 2))
h.waitForAppRunning()
})
})
Context("Mutate a Managed Resource (Scale Deployment)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo with replicaCount=2", func() {
h.deployApp()
h.waitForDeploymentReady()
})
It("should revert manual scaling back to 2 replicas", func() {
By("Scaling Deployment to 5 replicas via kubectl scale")
runCommandSucceed("kubectl", "scale", "deployment", "podinfo", "--replicas=5", "-n", h.namespace)
By("Verifying Deployment scaled to 5")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(*d.Spec.Replicas).Should(Equal(int32(5)))
}, 10*time.Second, time.Second).Should(Succeed())
By("Triggering force reconcile via annotation")
RequestReconcileNow(h.ctx, h.app)
By("Verifying KubeVela reverts replicas to 2")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(*d.Spec.Replicas).Should(Equal(int32(2)))
}, 120*time.Second, 3*time.Second).Should(Succeed())
h.waitForDeploymentReady()
})
})
Context("Add Extra Annotation/Label", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo successfully", func() {
h.deployApp()
h.waitForDeploymentReady()
})
It("should preserve user-added annotations and labels after reconciliation", func() {
By("Adding custom annotation via kubectl annotate")
runCommandSucceed("kubectl", "annotate", "deployment", "podinfo", "custom.io/test=test-value", "-n", h.namespace)
By("Adding custom label via kubectl label")
runCommandSucceed("kubectl", "label", "deployment", "podinfo", "extra.io/label=extra-value", "-n", h.namespace)
By("Waiting 2+ reconcile cycles")
time.Sleep(10 * time.Second)
By("Triggering reconciliation")
RequestReconcileNow(h.ctx, h.app)
h.waitForAppRunning()
By("Verifying annotation and label are preserved (3-way merge)")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(d.GetAnnotations()).Should(HaveKeyWithValue("custom.io/test", "test-value"))
g.Expect(d.GetLabels()).Should(HaveKeyWithValue("extra.io/label", "extra-value"))
}, 30*time.Second, 3*time.Second).Should(Succeed())
})
})
Context("Delete the Application CR", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanupNamespaceOnly() })
It("should deploy podinfo and perform 3 upgrades", func() {
h.deployApp()
h.waitForDeploymentReady()
h.updateAppValues(map[string]interface{}{"ui": map[string]interface{}{"message": "upgrade-1"}})
h.updateAppValues(map[string]interface{}{"ui": map[string]interface{}{"message": "upgrade-2"}})
h.updateAppValues(map[string]interface{}{"ui": map[string]interface{}{"message": "upgrade-3"}})
Expect(len(h.getHelmSecrets().Items)).Should(BeNumerically(">=", 1))
})
It("should clean up all resources when Application is deleted", func() {
appName := h.app.Name
By("Deleting Application via kubectl")
runCommandSucceed("kubectl", "delete", "application", appName, "-n", h.appNamespace)
By("Verifying Application is gone")
Eventually(func() bool {
return k8sClient.Get(h.ctx, h.appKey, &v1beta1.Application{}) != nil
}, 60*time.Second, 2*time.Second).Should(BeTrue())
h.app = nil
By("Verifying ALL Helm release secrets are deleted")
Eventually(func() int { return len(h.getHelmSecrets().Items) }, 60*time.Second, 3*time.Second).Should(Equal(0))
By("Verifying helm list shows empty")
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", h.namespace, "-q")
return strings.TrimSpace(out)
}, 30*time.Second, 3*time.Second).Should(BeEmpty())
By("Verifying Deployment, Service, and pods are all deleted")
Eventually(func() bool {
return k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, &appsv1.Deployment{}) != nil
}, 30*time.Second, 2*time.Second).Should(BeTrue())
Eventually(func() bool {
return k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, &corev1.Service{}) != nil
}, 30*time.Second, 2*time.Second).Should(BeTrue())
Eventually(func() int {
pods := &corev1.PodList{}
_ = k8sClient.List(h.ctx, pods, client.InNamespace(h.namespace),
client.MatchingLabels{"app.kubernetes.io/name": "podinfo"})
return len(pods.Items)
}, 60*time.Second, 2*time.Second).Should(Equal(0))
By("Verifying ResourceTracker is deleted")
Eventually(func() bool {
rtList := &v1beta1.ResourceTrackerList{}
Expect(k8sClient.List(h.ctx, rtList, client.MatchingLabels{"app.oam.dev/name": appName})).Should(Succeed())
return len(rtList.Items) == 0
}, 30*time.Second, 2*time.Second).Should(BeTrue())
})
})
Context("Delete a Non-Deployment Resource (Service)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo successfully", func() {
h.deployApp()
h.waitForDeploymentReady()
})
It("should recover when the Service is deleted via kubectl", func() {
originalPodUIDs := h.recordPodUIDs()
By("Recording old ClusterIP")
oldSvc := &corev1.Service{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, oldSvc)).Should(Succeed())
oldClusterIP := oldSvc.Spec.ClusterIP
By("Deleting the Service via kubectl")
runCommandSucceed("kubectl", "delete", "svc", "podinfo", "-n", h.namespace)
By("Triggering reconciliation")
RequestReconcileNow(h.ctx, h.app)
By("Verifying KubeVela recreates the Service with a new ClusterIP")
var newClusterIP string
Eventually(func(g Gomega) {
svc := &corev1.Service{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, svc)).Should(Succeed())
g.Expect(svc.Spec.ClusterIP).ShouldNot(BeEmpty())
newClusterIP = svc.Spec.ClusterIP
}, 120*time.Second, 3*time.Second).Should(Succeed())
Expect(newClusterIP).ShouldNot(Equal(oldClusterIP),
"New ClusterIP should be assigned after Service recreation")
By("Verifying pods are NOT affected")
Expect(h.countSurvivingPods(originalPodUIDs)).Should(BeNumerically(">=", 2))
})
})
Context("Delete the Namespace", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo successfully", func() {
h.deployApp()
h.waitForDeploymentReady()
})
It("should recover after namespace deletion", func() {
By("Deleting the target namespace via kubectl")
runCommandSucceed("kubectl", "delete", "namespace", h.namespace, "--wait=false")
By("Waiting for namespace to be fully deleted")
Eventually(func() bool {
return k8sClient.Get(h.ctx, types.NamespacedName{Name: h.namespace}, &corev1.Namespace{}) != nil
}, 120*time.Second, 3*time.Second).Should(BeTrue())
By("Verifying Application CR survives (it is in default namespace)")
Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
By("Applying a spec change to trigger re-render")
Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
annotations := h.app.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations["test.oam.dev/trigger"] = "ns-delete-recovery"
h.app.SetAnnotations(annotations)
Expect(k8sClient.Update(h.ctx, h.app)).Should(Succeed())
By("Verifying namespace is recreated (via createNamespace: true)")
Eventually(func(g Gomega) {
ns := &corev1.Namespace{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Name: h.namespace}, ns)).Should(Succeed())
g.Expect(ns.Status.Phase).Should(Equal(corev1.NamespaceActive))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Verifying all resources and Helm release are restored")
h.waitForDeploymentReady()
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", h.namespace, "-q")
return out
}, 60*time.Second, 3*time.Second).Should(ContainSubstring("podinfo"))
h.waitForAppRunning()
})
})
Context("Corrupt the Helm Release Secret", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy podinfo successfully", func() {
h.deployApp()
h.waitForDeploymentReady()
})
It("should recover from corrupted Helm release secret", func() {
originalPodUIDs := h.recordPodUIDs()
latestSecret := h.latestHelmSecretName()
Expect(latestSecret).ShouldNot(BeEmpty())
By("Corrupting the release secret via kubectl patch")
runCommandSucceed("kubectl", "patch", "secret", latestSecret, "-n", h.namespace,
"--type=json", `-p=[{"op":"replace","path":"/data/release","value":"Y29ycnVwdGVk"}]`)
By("Applying a spec change to trigger re-render")
Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
annotations := h.app.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations["test.oam.dev/trigger"] = "corrupt-recovery"
h.app.SetAnnotations(annotations)
Expect(k8sClient.Update(h.ctx, h.app)).Should(Succeed())
By("Verifying corrupted secret is automatically deleted")
Eventually(func() bool {
s := &corev1.Secret{}
err := k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: latestSecret}, s)
if err != nil {
return true
}
return string(s.Data["release"]) != "corrupted"
}, 60*time.Second, 3*time.Second).Should(BeTrue())
By("Verifying helm list shows a clean release")
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", h.namespace, "-q")
return out
}, 120*time.Second, 3*time.Second).Should(ContainSubstring("podinfo"))
h.waitForDeploymentReady()
h.waitForAppRunning()
By("Verifying pods are unaffected during recovery")
Expect(h.countSurvivingPods(originalPodUIDs)).Should(BeNumerically(">=", 2))
})
})
})
// ============================================================================
// Adoption & Takeover Scenarios
// ============================================================================
var _ = Describe("Helmchart Adoption & Takeover", func() {
Context("Adopt an Existing Vanilla Helm Release", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should adopt a pre-existing Helm release", func() {
By("Installing podinfo via helm install directly (no KubeVela)")
runCommandSucceed("helm", "install", "podinfo",
"--repo", "https://stefanprodan.github.io/podinfo", "podinfo",
"--version", "6.11.1", "--set", "replicaCount=2", "-n", h.namespace)
initialSecretCount := len(h.getHelmSecrets().Items)
By("Recording running pod UIDs before adoption")
var podList corev1.PodList
Eventually(func(g Gomega) {
g.Expect(k8sClient.List(h.ctx, &podList, client.InNamespace(h.namespace),
client.MatchingLabels{"app.kubernetes.io/name": "podinfo"})).Should(Succeed())
g.Expect(len(podList.Items)).Should(BeNumerically(">=", 2))
}, 60*time.Second, 3*time.Second).Should(Succeed())
originalPodUIDs := make(map[types.UID]bool)
for _, pod := range podList.Items {
originalPodUIDs[pod.UID] = true
}
By("Applying a KubeVela Application with same release name, chart, and values")
h.deployApp()
h.waitForAppRunning()
By("Verifying Helm revision increments by 1 (forced upgrade to inject KubeVela labels)")
Expect(len(h.getHelmSecrets().Items)).Should(Equal(initialSecretCount + 1))
By("Verifying pods are NOT restarted (zero downtime adoption)")
Expect(h.countSurvivingPods(originalPodUIDs)).Should(BeNumerically(">=", 2))
By("Verifying app.oam.dev/* labels appear on Deployment")
deploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
Expect(deploy.GetLabels()).Should(HaveKey("app.oam.dev/name"))
By("Verifying app.oam.dev/* labels appear on Service")
svc := &corev1.Service{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, svc)).Should(Succeed())
Expect(svc.GetLabels()).Should(HaveKey("app.oam.dev/name"))
By("Verifying meta.helm.sh/release-name annotation is preserved")
Expect(deploy.GetAnnotations()).Should(HaveKeyWithValue("meta.helm.sh/release-name", "podinfo"))
})
})
Context("Adopt Release with Different Values", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should adopt and upgrade a release with different values", func() {
By("Installing podinfo via helm install with replicaCount=1")
runCommandSucceed("helm", "install", "podinfo",
"--repo", "https://stefanprodan.github.io/podinfo", "podinfo",
"--version", "6.11.1", "--set", "replicaCount=1", "-n", h.namespace)
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(Equal(int32(1)))
}, 60*time.Second, 3*time.Second).Should(Succeed())
initialSecretCount := len(h.getHelmSecrets().Items)
By("Applying KubeVela Application with replicaCount=3")
raw, err := os.ReadFile("testdata/helm/app_helmchart_podinfo.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
raw = bytes.ReplaceAll(raw, []byte("replicaCount: 2"), []byte("replicaCount: 3"))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("podinfo-helm-test-" + rand.RandomString(4))
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Verifying Helm upgrade occurs (fingerprint differs)")
Expect(len(h.getHelmSecrets().Items)).Should(BeNumerically(">", initialSecretCount))
By("Verifying replicas scale to 3")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(Equal(int32(3)))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Verifying KubeVela labels injected")
deploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
Expect(deploy.GetLabels()).Should(HaveKey("app.oam.dev/name"))
})
})
Context("Re-adopt After Application Deletion", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanupNamespaceOnly() })
It("should re-adopt seamlessly after deletion and reinstall", func() {
By("Installing podinfo via helm install")
runCommandSucceed("helm", "install", "podinfo",
"--repo", "https://stefanprodan.github.io/podinfo", "podinfo",
"--version", "6.11.1", "--set", "replicaCount=2", "-n", h.namespace)
By("Applying KubeVela Application (adopts the release)")
h.deployApp()
h.waitForAppRunning()
By("Deleting the Application (GC cleans up everything)")
runCommandSucceed("kubectl", "delete", "application", h.app.Name, "-n", h.appNamespace)
Eventually(func() bool {
return k8sClient.Get(h.ctx, h.appKey, &v1beta1.Application{}) != nil
}, 60*time.Second, 2*time.Second).Should(BeTrue())
h.app = nil
By("Waiting for GC to clean up resources")
Eventually(func() int { return len(h.getHelmSecrets().Items) }, 60*time.Second, 3*time.Second).Should(Equal(0))
By("Installing podinfo via helm install again")
runCommandSucceed("helm", "install", "podinfo",
"--repo", "https://stefanprodan.github.io/podinfo", "podinfo",
"--version", "6.11.1", "--set", "replicaCount=2", "-n", h.namespace)
By("Applying the same KubeVela Application again")
h.deployApp()
h.waitForAppRunning()
h.waitForDeploymentReady()
})
})
})
// ============================================================================
// Helm State Integrity Scenarios
// ============================================================================
var _ = Describe("Helmchart State Integrity", func() {
Context("Upgrade History Preserved Across Multiple Changes", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should preserve upgrade history across 5 changes", func() {
h.deployApp()
h.waitForDeploymentReady()
for i := 1; i <= 5; i++ {
prevSecretCount := len(h.getHelmSecrets().Items)
By(fmt.Sprintf("Upgrade %d: changing values and waiting for new helm revision", i))
h.updateAppValues(map[string]interface{}{"ui": map[string]interface{}{"message": fmt.Sprintf("upgrade-%d", i)}})
Eventually(func() int {
return len(h.getHelmSecrets().Items)
}, 120*time.Second, 3*time.Second).Should(BeNumerically(">", prevSecretCount),
fmt.Sprintf("Upgrade %d: expected helm revision to increment", i))
}
By("Verifying helm history shows all 6 revisions (1 install + 5 upgrades)")
out := runCommandSucceed("helm", "history", "podinfo", "-n", h.namespace, "--output", "json")
var history []map[string]interface{}
Expect(json.Unmarshal([]byte(out), &history)).Should(Succeed())
Expect(len(history)).Should(BeNumerically(">=", 6),
"Expected at least 6 revisions (1 install + 5 upgrades)")
By("Verifying all release secrets exist")
Expect(len(h.getHelmSecrets().Items)).Should(BeNumerically(">=", 6))
By("Verifying maxHistory is respected (default maxHistory=10 in chart options)")
Expect(len(h.getHelmSecrets().Items)).Should(BeNumerically("<=", 10))
})
})
})
// ============================================================================
// Destructive & Chaos Scenarios
// ============================================================================
var _ = Describe("Helmchart Destructive & Chaos", func() {
Context("Two Applications Targeting Same Release Name", Ordered, func() {
h := newHelmTestContext()
var appB *v1beta1.Application
BeforeAll(func() { h.createNamespace() })
AfterAll(func() {
if appB != nil {
_ = k8sClient.Delete(h.ctx, appB)
}
h.cleanup()
})
It("should detect ownership conflict", func() {
h.deployApp()
h.waitForDeploymentReady()
h.waitForAppRunning()
By("Deploying second Application also targeting release podinfo")
raw, err := os.ReadFile("testdata/helm/app_helmchart_podinfo.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
appB = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, appB)).Should(BeNil())
appB.SetNamespace(h.appNamespace)
appB.SetName("podinfo-conflict-" + rand.RandomString(4))
Expect(k8sClient.Create(h.ctx, appB)).Should(Succeed())
appBKey := client.ObjectKeyFromObject(appB)
By("Verifying second application fails with ownership conflict")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, appBKey, appB)).Should(Succeed())
g.Expect(appB.Status.Phase).Should(SatisfyAny(
Equal(common2.ApplicationWorkflowFailed),
Equal(common2.ApplicationUnhealthy),
Equal(common2.ApplicationRunning),
))
}, 60*time.Second, 3*time.Second).Should(Succeed())
By("Verifying the first application remains healthy and unaffected")
h.waitForAppRunning()
h.waitForDeploymentReady()
})
})
})
// ============================================================================
// Resource Ordering Scenarios
// ============================================================================
var _ = Describe("Helmchart Resource Ordering", func() {
Context("Chart with CRDs (crossplane)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanupNamespaceOnly() })
It("should deploy crossplane chart with CRDs and reach running", func() {
By("Deploying crossplane chart (includes CRDs)")
raw := []byte(fmt.Sprintf(`apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: crossplane-crd-test
spec:
components:
- name: crossplane
type: helmchart
properties:
chart:
source: crossplane
repoURL: https://charts.crossplane.io/stable
version: "1.19.1"
release:
name: crossplane
namespace: %s
values:
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
args:
- --debug=false
options:
createNamespace: true
includeCRDs: true
skipTests: true`, h.namespace))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("crossplane-crd-test-" + rand.RandomString(4))
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
By("Verifying Application reaches running")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 300*time.Second, 5*time.Second).Should(Succeed())
By("Verifying crossplane Deployment is ready")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "crossplane"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(BeNumerically(">=", 1))
}, 120*time.Second, 3*time.Second).Should(Succeed())
})
It("should clean up CRDs and all resources on deletion", func() {
By("Deleting the Application")
runCommandSucceed("kubectl", "delete", "application", h.app.Name, "-n", h.appNamespace)
Eventually(func() bool {
return k8sClient.Get(h.ctx, h.appKey, &v1beta1.Application{}) != nil
}, 60*time.Second, 2*time.Second).Should(BeTrue())
h.app = nil
By("Verifying crossplane Deployment is cleaned up")
Eventually(func() bool {
return k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "crossplane"}, &appsv1.Deployment{}) != nil
}, 60*time.Second, 3*time.Second).Should(BeTrue())
By("Verifying helm release secrets are cleaned up")
Eventually(func() int {
secrets := &corev1.SecretList{}
_ = k8sClient.List(h.ctx, secrets, client.InNamespace(h.namespace),
client.MatchingLabels{"owner": "helm", "name": "crossplane"})
return len(secrets.Items)
}, 60*time.Second, 3*time.Second).Should(Equal(0))
})
})
Context("Chart with Namespaces (createNamespace)", Ordered, func() {
h := newHelmTestContext()
AfterAll(func() { h.cleanup() })
It("should create namespace before deploying namespace-scoped resources", func() {
By("Verifying target namespace does not exist yet")
err := k8sClient.Get(h.ctx, types.NamespacedName{Name: h.namespace}, &corev1.Namespace{})
Expect(err).ShouldNot(BeNil())
h.deployApp()
By("Verifying namespace was created")
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Name: h.namespace}, &corev1.Namespace{})).Should(Succeed())
h.waitForDeploymentReady()
})
})
})
// ============================================================================
// Health Check Scenarios
// ============================================================================
var _ = Describe("Helmchart Health Checks", func() {
Context("Custom Health Check — Deployment Available", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should report healthy when deployment is available", func() {
h.deployAppFrom("testdata/helm/app_helmchart_podinfo_health.yaml")
h.waitForDeploymentReady()
Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
})
It("should self-heal when scaled to 0", func() {
By("Scaling deployment to 0 manually")
runCommandSucceed("kubectl", "scale", "deployment", "podinfo", "--replicas=0", "-n", h.namespace)
By("Verifying deployment scaled to 0")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(Equal(int32(0)))
}, 30*time.Second, 2*time.Second).Should(Succeed())
By("Triggering reconciliation to detect and fix the drift")
RequestReconcileNow(h.ctx, h.app)
By("Verifying KubeVela self-heals replicas back to 2")
h.waitForDeploymentReady()
h.waitForAppRunning()
})
})
Context("Custom Health Check — Multiple Criteria (Two Components)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should be healthy when both podinfo-a and podinfo-b Deployments are Available", func() {
h.deployAppFrom("testdata/helm/app_helmchart_podinfo_multi_health.yaml")
By("Waiting for both Deployments to be ready")
for _, name := range []string{"podinfo-a", "podinfo-b"} {
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: name}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(BeNumerically(">=", 1))
}, 120*time.Second, 3*time.Second).Should(Succeed())
}
h.waitForAppRunning()
})
It("should detect unhealthy when one Deployment is deleted and self-heal", func() {
By("Deleting podinfo-a Deployment (podinfo-b is still healthy)")
runCommandSucceed("kubectl", "delete", "deployment", "podinfo-a", "-n", h.namespace)
By("Verifying podinfo-a Deployment is gone")
Eventually(func() bool {
return k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo-a"}, &appsv1.Deployment{}) != nil
}, 10*time.Second, time.Second).Should(BeTrue())
By("Verifying podinfo-b Deployment is still running")
d := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo-b"}, d)).Should(Succeed())
Expect(d.Status.ReadyReplicas).Should(BeNumerically(">=", 1))
By("Triggering reconciliation")
RequestReconcileNow(h.ctx, h.app)
By("Verifying KubeVela self-heals by recreating podinfo-a Deployment")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo-a"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(BeNumerically(">=", 1))
}, 120*time.Second, 3*time.Second).Should(Succeed())
h.waitForAppRunning()
})
})
Context("No Health Check Defined", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should default to healthy when no healthStatus field", func() {
h.deployAppFrom("testdata/helm/app_helmchart_podinfo_no_values.yaml")
h.waitForAppRunning()
})
})
})
// ============================================================================
// Edge Cases & Boundary Conditions
// ============================================================================
var _ = Describe("Helmchart Edge Cases", func() {
Context("Empty Values", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should install chart with default values when no values field", func() {
h.deployAppFrom("testdata/helm/app_helmchart_podinfo_no_values.yaml")
h.waitForAppRunning()
deploy := &appsv1.Deployment{}
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
g.Expect(deploy.Status.ReadyReplicas).Should(BeNumerically(">=", 1))
}, 120*time.Second, 3*time.Second).Should(Succeed())
})
})
Context("Namespace Does Not Exist and createNamespace=false", Ordered, func() {
h := &helmTestContext{
ctx: context.Background(),
namespace: "nonexistent-ns-" + rand.RandomString(4),
appNamespace: "default",
}
AfterAll(func() {
if h.app != nil {
_ = k8sClient.Delete(h.ctx, h.app)
}
})
It("should fail when namespace does not exist", func() {
raw, err := os.ReadFile("testdata/helm/app_helmchart_podinfo_no_create_ns.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("no-ns-test-" + rand.RandomString(4))
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(SatisfyAny(
Equal(common2.ApplicationWorkflowFailed),
Equal(common2.ApplicationUnhealthy),
))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Verifying namespace was not created")
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Name: h.namespace}, &corev1.Namespace{})).ShouldNot(Succeed())
})
})
Context("Chart Not Found in Repository", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() {
if h.app != nil {
_ = k8sClient.Delete(h.ctx, h.app)
}
h.cleanupNamespaceOnly()
})
It("should fail with clear error for non-existent chart", func() {
raw, err := os.ReadFile("testdata/helm/app_helmchart_bad_chart.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("bad-chart-test-" + rand.RandomString(4))
createErr := k8sClient.Create(h.ctx, h.app)
if createErr != nil {
By("Webhook dry-run caught the bad chart — rejected at admission")
Expect(createErr.Error()).Should(ContainSubstring("not found"))
h.app = nil // not created, nothing to clean up
} else {
By("Webhook did not catch it — waiting for workflow failure")
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(SatisfyAny(
Equal(common2.ApplicationWorkflowFailed),
Equal(common2.ApplicationUnhealthy),
))
}, 120*time.Second, 3*time.Second).Should(Succeed())
}
})
})
Context("Invalid Chart Version", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should fail with bad version then succeed with correct version", func() {
raw, err := os.ReadFile("testdata/helm/app_helmchart_bad_version.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("bad-version-test-" + rand.RandomString(4))
createErr := k8sClient.Create(h.ctx, h.app)
if createErr != nil {
By("Webhook dry-run caught the bad version — rejected at admission")
Expect(createErr.Error()).Should(ContainSubstring("not found"))
By("Creating with correct version directly")
h.app.SetResourceVersion("")
rawProps, _ := json.Marshal(h.app.Spec.Components[0].Properties)
var props map[string]interface{}
_ = json.Unmarshal(rawProps, &props)
chart := props["chart"].(map[string]interface{})
chart["version"] = "6.11.1"
props["chart"] = chart
newRaw, _ := json.Marshal(props)
h.app.Spec.Components[0].Properties = &runtime.RawExtension{Raw: newRaw}
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
} else {
By("Webhook did not catch it — waiting for workflow failure then updating")
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(SatisfyAny(
Equal(common2.ApplicationWorkflowFailed),
Equal(common2.ApplicationUnhealthy),
))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Updating to correct version")
Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
rawProps, err := json.Marshal(h.app.Spec.Components[0].Properties)
Expect(err).Should(BeNil())
var props map[string]interface{}
Expect(json.Unmarshal(rawProps, &props)).Should(BeNil())
chart := props["chart"].(map[string]interface{})
chart["version"] = "6.11.1"
props["chart"] = chart
newRaw, err := json.Marshal(props)
Expect(err).Should(BeNil())
h.app.Spec.Components[0].Properties = &runtime.RawExtension{Raw: newRaw}
Expect(k8sClient.Update(h.ctx, h.app)).Should(Succeed())
}
h.waitForAppRunning()
})
})
Context("Two helmchart Components in Same Application", Ordered, func() {
h := newHelmTestContext()
nsA := "helm-multi-a-" + rand.RandomString(4)
nsB := "helm-multi-b-" + rand.RandomString(4)
BeforeAll(func() {
for _, ns := range []string{nsA, nsB} {
n := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}
Expect(k8sClient.Create(h.ctx, n)).Should(SatisfyAny(Succeed(), Not(HaveOccurred())))
}
})
AfterAll(func() {
if h.app != nil {
_ = k8sClient.Delete(h.ctx, h.app)
Eventually(func() bool {
return k8sClient.Get(h.ctx, h.appKey, &v1beta1.Application{}) != nil
}, 60*time.Second, 2*time.Second).Should(BeTrue())
}
for _, ns := range []string{nsA, nsB} {
n := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}
_ = k8sClient.Delete(h.ctx, n, client.PropagationPolicy(metav1.DeletePropagationForeground))
}
})
It("should manage podinfo and crossplane as independent Helm releases", func() {
raw, err := os.ReadFile("testdata/helm/app_helmchart_two_components.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns_a"), []byte(nsA))
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns_b"), []byte(nsB))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("two-comp-test-" + rand.RandomString(4))
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 300*time.Second, 5*time.Second).Should(Succeed())
By("Verifying podinfo release exists in namespace A")
out, _ := runCommand("helm", "list", "-n", nsA, "-q")
Expect(out).Should(ContainSubstring("podinfo"))
By("Verifying crossplane release exists in namespace B")
out, _ = runCommand("helm", "list", "-n", nsB, "-q")
Expect(out).Should(ContainSubstring("crossplane"))
By("Verifying podinfo Deployment is ready in namespace A")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: nsA, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(Equal(int32(1)))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Verifying crossplane Deployment is ready in namespace B")
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: nsB, Name: "crossplane"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(BeNumerically(">=", 1))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Verifying crossplane release secrets exist")
cpSecrets := &corev1.SecretList{}
Expect(k8sClient.List(h.ctx, cpSecrets,
client.InNamespace(nsB),
client.MatchingLabels{"owner": "helm", "name": "crossplane"},
)).Should(Succeed())
Expect(len(cpSecrets.Items)).Should(BeNumerically(">=", 1))
})
It("should not affect crossplane when upgrading podinfo component", func() {
By("Recording crossplane Deployment replica count and image before podinfo upgrade")
cpDeploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: nsB, Name: "crossplane"}, cpDeploy)).Should(Succeed())
cpImage := cpDeploy.Spec.Template.Spec.Containers[0].Image
cpReplicas := *cpDeploy.Spec.Replicas
By("Upgrading podinfo component values")
Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
rawProps, err := json.Marshal(h.app.Spec.Components[0].Properties)
Expect(err).Should(BeNil())
var props map[string]interface{}
Expect(json.Unmarshal(rawProps, &props)).Should(BeNil())
if vals, ok := props["values"].(map[string]interface{}); ok {
vals["ui"] = map[string]interface{}{"message": "upgraded-podinfo"}
}
newRaw, err := json.Marshal(props)
Expect(err).Should(BeNil())
h.app.Spec.Components[0].Properties = &runtime.RawExtension{Raw: newRaw}
Expect(k8sClient.Update(h.ctx, h.app)).Should(Succeed())
By("Waiting for Application to return to running after upgrade")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 180*time.Second, 3*time.Second).Should(Succeed())
By("Verifying crossplane Deployment spec was NOT affected (image and replicas unchanged)")
cpDeploy = &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: nsB, Name: "crossplane"}, cpDeploy)).Should(Succeed())
Expect(cpDeploy.Spec.Template.Spec.Containers[0].Image).Should(Equal(cpImage),
"crossplane image should not change when podinfo is upgraded")
Expect(*cpDeploy.Spec.Replicas).Should(Equal(cpReplicas),
"crossplane replicas should not change when podinfo is upgraded")
})
It("should clean up both releases when Application is deleted", func() {
appName := h.app.Name
runCommandSucceed("kubectl", "delete", "application", appName, "-n", h.appNamespace)
Eventually(func() bool {
return k8sClient.Get(h.ctx, h.appKey, &v1beta1.Application{}) != nil
}, 60*time.Second, 2*time.Second).Should(BeTrue())
h.app = nil
By("Verifying podinfo release cleaned up in namespace A")
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", nsA, "-q")
return strings.TrimSpace(out)
}, 60*time.Second, 3*time.Second).Should(BeEmpty())
By("Verifying crossplane release cleaned up in namespace B")
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", nsB, "-q")
return strings.TrimSpace(out)
}, 60*time.Second, 3*time.Second).Should(BeEmpty())
By("Verifying crossplane release secrets are deleted")
Eventually(func() int {
cpSecrets := &corev1.SecretList{}
Expect(k8sClient.List(h.ctx, cpSecrets,
client.InNamespace(nsB),
client.MatchingLabels{"owner": "helm", "name": "crossplane"},
)).Should(Succeed())
return len(cpSecrets.Items)
}, 60*time.Second, 3*time.Second).Should(Equal(0))
})
})
Context("Helm Release Exists with Different Chart", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should detect chart mismatch and upgrade to podinfo", func() {
By("Installing podinfo v6.11.0 as release 'myrelease' via helm install")
runCommandSucceed("helm", "install", "myrelease",
"--repo", "https://stefanprodan.github.io/podinfo", "podinfo",
"--version", "6.11.0", "--set", "replicaCount=1", "-n", h.namespace)
Eventually(func() string {
out, _ := runCommand("helm", "list", "-n", h.namespace, "-q")
return out
}, 30*time.Second, 3*time.Second).Should(ContainSubstring("myrelease"))
By("Recording old resources from v6.11.0")
oldDeploy := &appsv1.Deployment{}
Eventually(func(g Gomega) {
deployList := &appsv1.DeploymentList{}
g.Expect(k8sClient.List(h.ctx, deployList, client.InNamespace(h.namespace))).Should(Succeed())
g.Expect(len(deployList.Items)).Should(BeNumerically(">=", 1))
oldDeploy = &deployList.Items[0]
}, 60*time.Second, 3*time.Second).Should(Succeed())
oldDeployName := oldDeploy.Name
By("Applying KubeVela Application with podinfo v6.11.1 targeting 'myrelease'")
raw, err := os.ReadFile("testdata/helm/app_helmchart_podinfo.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
raw = bytes.ReplaceAll(raw, []byte("name: podinfo\n namespace"), []byte("name: myrelease\n namespace"))
h.app = &v1beta1.Application{}
Expect(yaml.Unmarshal(raw, h.app)).Should(BeNil())
h.app.SetNamespace(h.appNamespace)
h.app.SetName("chart-mismatch-" + rand.RandomString(4))
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 180*time.Second, 3*time.Second).Should(Succeed())
By("Verifying KubeVela labels injected on deployment")
deployList := &appsv1.DeploymentList{}
Expect(k8sClient.List(h.ctx, deployList, client.InNamespace(h.namespace))).Should(Succeed())
Expect(len(deployList.Items)).Should(BeNumerically(">=", 1))
Expect(deployList.Items[0].GetLabels()).Should(HaveKey("app.oam.dev/name"))
By("Verifying old resources were replaced (deployment name from old release: " + oldDeployName + ")")
_ = oldDeployName
})
})
Context("Apply Same Application Twice Without Changes", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should not trigger Helm upgrade when no changes", func() {
h.deployApp()
h.waitForDeploymentReady()
h.waitForAppRunning()
initialSecretCount := len(h.getHelmSecrets().Items)
latestSecret := h.latestHelmSecretName()
deploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
initialResourceVersion := deploy.ResourceVersion
By("Applying the exact same manifest via kubectl apply")
raw, err := os.ReadFile("testdata/helm/app_helmchart_podinfo.yaml")
Expect(err).Should(BeNil())
raw = bytes.ReplaceAll(raw, []byte("placeholder_ns"), []byte(h.namespace))
raw = bytes.ReplaceAll(raw, []byte("name: podinfo-helm-test"), []byte("name: "+h.app.Name))
tmpFile := fmt.Sprintf("/tmp/helm-reapply-%s.yaml", rand.RandomString(4))
Expect(os.WriteFile(tmpFile, raw, 0644)).Should(Succeed())
defer os.Remove(tmpFile)
runCommandSucceed("kubectl", "apply", "-f", tmpFile, "-n", h.appNamespace)
time.Sleep(10 * time.Second)
By("Verifying no Helm upgrade occurred")
Expect(len(h.getHelmSecrets().Items)).Should(Equal(initialSecretCount))
Expect(h.latestHelmSecretName()).Should(Equal(latestSecret))
By("Verifying Deployment resourceVersion is stable")
deploy = &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
Expect(deploy.ResourceVersion).Should(Equal(initialResourceVersion))
})
})
})
// ============================================================================
// valuesFrom Tests Scenarios
// ============================================================================
var _ = Describe("Helmchart valuesFrom", func() {
createCM := func(h *helmTestContext, name, key, valuesYAML string) {
if key == "" {
key = "values.yaml"
}
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: h.namespace},
Data: map[string]string{key: valuesYAML},
}
Expect(k8sClient.Create(h.ctx, cm)).Should(Succeed())
}
createCMWithReplicas := func(h *helmTestContext, name string, replicaCount int) {
createCM(h, name, "", fmt.Sprintf("replicaCount: %d\n", replicaCount))
}
createCMInNamespace := func(h *helmTestContext, name, ns, valuesYAML string) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},
Data: map[string]string{"values.yaml": valuesYAML},
}
Expect(k8sClient.Create(h.ctx, cm)).Should(Succeed())
}
createSecret := func(h *helmTestContext, name, valuesYAML string) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: h.namespace},
Data: map[string][]byte{"values.yaml": []byte(valuesYAML)},
}
Expect(k8sClient.Create(h.ctx, secret)).Should(Succeed())
}
createSecretWithReplicas := func(h *helmTestContext, name string, replicaCount int) {
createSecret(h, name, fmt.Sprintf("replicaCount: %d\n", replicaCount))
}
buildPodinfoComponent := func(h *helmTestContext, componentName, releaseName string, props map[string]interface{}) common2.ApplicationComponent {
merged := map[string]interface{}{
"chart": map[string]interface{}{
"source": "podinfo",
"repoURL": "https://stefanprodan.github.io/podinfo",
"version": "6.11.1",
},
"release": map[string]interface{}{
"name": releaseName,
"namespace": h.namespace,
},
"options": map[string]interface{}{
"createNamespace": true,
"skipTests": true,
},
}
for k, v := range props {
merged[k] = v
}
raw, err := json.Marshal(merged)
Expect(err).ShouldNot(HaveOccurred())
return common2.ApplicationComponent{
Name: componentName,
Type: "helmchart",
Properties: &runtime.RawExtension{Raw: raw},
}
}
deployAppWithComponents := func(h *helmTestContext, appNamePrefix string, comps []common2.ApplicationComponent) {
h.app = &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: appNamePrefix + "-" + rand.RandomString(4),
Namespace: h.appNamespace,
},
Spec: v1beta1.ApplicationSpec{Components: comps},
}
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Phase).Should(Equal(common2.ApplicationRunning))
}, 120*time.Second, 3*time.Second).Should(Succeed())
}
deployPodinfo := func(h *helmTestContext, appNamePrefix, releaseName string, props map[string]interface{}) {
comp := buildPodinfoComponent(h, "podinfo", releaseName, props)
deployAppWithComponents(h, appNamePrefix, []common2.ApplicationComponent{comp})
}
deployPodinfoExpectWorkflowFailure := func(h *helmTestContext, appNamePrefix, releaseName string, props map[string]interface{}, errSubstring string) {
comp := buildPodinfoComponent(h, "podinfo", releaseName, props)
h.app = &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: appNamePrefix + "-" + rand.RandomString(4),
Namespace: h.appNamespace,
},
Spec: v1beta1.ApplicationSpec{Components: []common2.ApplicationComponent{comp}},
}
Expect(k8sClient.Create(h.ctx, h.app)).Should(Succeed())
h.appKey = client.ObjectKeyFromObject(h.app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Workflow).ToNot(BeNil())
g.Expect(string(h.app.Status.Workflow.Phase)).To(Equal("failed"))
var found bool
for _, step := range h.app.Status.Workflow.Steps {
if strings.Contains(step.Message, errSubstring) {
found = true
break
}
}
g.Expect(found).To(BeTrue(),
"no workflow step contained %q; status=%+v", errSubstring, h.app.Status.Workflow)
}, 180*time.Second, 5*time.Second).Should(Succeed())
err := k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, &appsv1.Deployment{})
Expect(err).To(HaveOccurred(), "no Deployment should exist for a failed workflow")
}
waitForReplicas := func(h *helmTestContext, want int32) {
Eventually(func(g Gomega) {
deploy := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
g.Expect(deploy.Status.ReadyReplicas).Should(Equal(want))
}, 120*time.Second, 3*time.Second).Should(Succeed())
}
waitForNamedReplicas := func(h *helmTestContext, deployName string, want int32) {
Eventually(func(g Gomega) {
deploy := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: deployName}, deploy)).Should(Succeed())
g.Expect(deploy.Status.ReadyReplicas).Should(Equal(want))
}, 120*time.Second, 3*time.Second).Should(Succeed())
}
cmRef := func(name string, opts ...map[string]interface{}) map[string]interface{} {
entry := map[string]interface{}{"kind": "ConfigMap", "name": name}
for _, o := range opts {
for k, v := range o {
entry[k] = v
}
}
return entry
}
secretRef := func(name string, opts ...map[string]interface{}) map[string]interface{} {
entry := map[string]interface{}{"kind": "Secret", "name": name}
for _, o := range opts {
for k, v := range o {
entry[k] = v
}
}
return entry
}
Context("Values from ConfigMap", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should merge values from the referenced ConfigMap", func() {
createCMWithReplicas(h, "podinfo-values", 3)
deployPodinfo(h, "s27", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{cmRef("podinfo-values")},
})
waitForReplicas(h, 3)
})
})
Context("Values from Secret", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should merge values from the referenced Secret", func() {
createSecretWithReplicas(h, "podinfo-values", 2)
deployPodinfo(h, "s28", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{secretRef("podinfo-values")},
})
waitForReplicas(h, 2)
})
})
Context("Inline values override ConfigMap-supplied values", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should use inline replicaCount when it also appears in the ConfigMap", func() {
createCMWithReplicas(h, "podinfo-values", 2)
deployPodinfo(h, "s29", "podinfo", map[string]interface{}{
"values": map[string]interface{}{"replicaCount": 4},
"valuesFrom": []interface{}{cmRef("podinfo-values")},
})
waitForReplicas(h, 4)
})
})
Context("Optional missing valuesFrom source is skipped", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy successfully even when the optional ConfigMap is missing", func() {
deployPodinfo(h, "s30", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("never-created", map[string]interface{}{"optional": true}),
},
})
waitForReplicas(h, 1) // chart default
})
})
Context("Required missing valuesFrom source fails the workflow with a clear error", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should fail the workflow and surface the missing-CM error", func() {
deployPodinfoExpectWorkflowFailure(h, "s31", "podinfo",
map[string]interface{}{
"valuesFrom": []interface{}{cmRef("never-created")},
},
`ConfigMap "never-created"`)
})
})
Context("Invalid YAML in a source is never swallowed by optional:true", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should surface the parse error even when the source is marked optional", func() {
createCM(h, "podinfo-bad-yaml", "", "replicaCount: [unterminated")
deployPodinfoExpectWorkflowFailure(h, "s32", "podinfo",
map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("podinfo-bad-yaml", map[string]interface{}{"optional": true}),
},
},
"invalid YAML")
})
})
Context("Custom key selects the right values.yaml inside a multi-env ConfigMap", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should use the specified key and ignore other keys in the ConfigMap", func() {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "podinfo-multi-env-values", Namespace: h.namespace},
Data: map[string]string{
"dev.yaml": "replicaCount: 1\n",
"prod.yaml": "replicaCount: 5\n",
},
}
Expect(k8sClient.Create(h.ctx, cm)).Should(Succeed())
deployPodinfo(h, "s33", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("podinfo-multi-env-values", map[string]interface{}{"key": "prod.yaml"}),
},
})
waitForReplicas(h, 5)
})
})
Context("Cross-namespace valuesFrom references are rejected", Ordered, func() {
h := newHelmTestContext()
otherNS := "helm-other-tenant-" + rand.RandomString(4)
BeforeAll(func() {
h.createNamespace()
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: otherNS}}
Expect(k8sClient.Create(h.ctx, ns)).Should(Succeed())
// Put a real ConfigMap in the other namespace so the failure is
// due to the cross-namespace guard, not a NotFound.
createCMInNamespace(h, "podinfo-values", otherNS, "replicaCount: 3\n")
})
AfterAll(func() {
h.cleanup()
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: otherNS}}
_ = k8sClient.Delete(h.ctx, ns, client.PropagationPolicy(metav1.DeletePropagationForeground))
})
It("should fail the workflow when valuesFrom.namespace != Application namespace", func() {
deployPodinfoExpectWorkflowFailure(h, "s34", "podinfo",
map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("podinfo-values", map[string]interface{}{"namespace": otherNS}),
},
},
"cross-namespace valuesFrom")
})
})
Context("Two ConfigMaps in valuesFrom — later overrides earlier on conflict", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should resolve replicaCount to the later CM's value", func() {
createCMWithReplicas(h, "podinfo-base-values", 2)
createCMWithReplicas(h, "podinfo-overlay-values", 4)
deployPodinfo(h, "s35", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("podinfo-base-values"),
cmRef("podinfo-overlay-values"),
},
})
waitForReplicas(h, 4)
})
})
Context("Deep merge preserves orthogonal nested keys across sources", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should keep base sibling keys when the overlay touches only one field in a nested map", func() {
createCM(h, "podinfo-base-values", "", `resources:
limits:
cpu: 100m
memory: 256Mi
requests:
cpu: 50m
replicaCount: 2
`)
createCM(h, "podinfo-overlay-values", "", `resources:
limits:
memory: 512Mi
`)
deployPodinfo(h, "s36", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("podinfo-base-values"),
cmRef("podinfo-overlay-values"),
},
})
waitForReplicas(h, 2)
deploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
limits := deploy.Spec.Template.Spec.Containers[0].Resources.Limits
requests := deploy.Spec.Template.Spec.Containers[0].Resources.Requests
Expect(limits.Memory().String()).To(Equal("512Mi"),
"overlay memory should win on direct conflict")
Expect(limits.Cpu().String()).To(Equal("100m"),
"base cpu should survive because overlay only touched memory")
Expect(requests.Cpu().String()).To(Equal("50m"),
"untouched requests.cpu from base must survive")
})
})
Context("Mixed ConfigMap and Secret in the same valuesFrom list", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should merge values from a ConfigMap followed by a Secret", func() {
createCM(h, "podinfo-cm-values", "", "replicaCount: 2\nimage:\n tag: 6.11.0\n")
createSecret(h, "podinfo-secret-values", "replicaCount: 3\n")
deployPodinfo(h, "s37", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("podinfo-cm-values"),
secretRef("podinfo-secret-values"),
},
})
waitForReplicas(h, 3)
})
})
Context("Two Secrets in valuesFrom — later overrides earlier", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should resolve conflicts between two Secrets with later-wins", func() {
createSecretWithReplicas(h, "podinfo-secret-a", 1)
createSecretWithReplicas(h, "podinfo-secret-b", 4)
deployPodinfo(h, "s38", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
secretRef("podinfo-secret-a"),
secretRef("podinfo-secret-b"),
},
})
waitForReplicas(h, 4)
})
})
Context("Application with only valuesFrom and no inline values", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy using values sourced entirely from the ConfigMap", func() {
createCMWithReplicas(h, "podinfo-values", 2)
deployPodinfo(h, "s39", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{cmRef("podinfo-values")},
})
waitForReplicas(h, 2)
})
})
Context("Empty valuesFrom list behaves like no valuesFrom", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should deploy at chart defaults when valuesFrom is an empty list", func() {
deployPodinfo(h, "s40", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{},
})
waitForReplicas(h, 1) // chart default
})
})
Context("Optional missing source is skipped, subsequent required source is still applied", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should skip the missing optional CM and use the following required CM's values", func() {
createCMWithReplicas(h, "podinfo-real-values", 3)
deployPodinfo(h, "s41", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("never-created", map[string]interface{}{"optional": true}),
cmRef("podinfo-real-values"),
},
})
waitForReplicas(h, 3)
})
})
Context("Non-existent explicit namespace fails required lookup cleanly", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should fail with a not-found error referencing the missing namespace", func() {
deployPodinfoExpectWorkflowFailure(h, "s42", "podinfo",
map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("any-cm", map[string]interface{}{"namespace": "does-not-exist-ns"}),
},
},
"does-not-exist-ns")
})
})
Context("Array values are replaced wholesale by the later source (Helm semantics)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should drop base array entries when the overlay sets the same array", func() {
createCM(h, "podinfo-extraargs-base", "",
"extraArgs:\n - --level=debug\n - --timeout=30\n")
createCM(h, "podinfo-extraargs-overlay", "",
"extraArgs:\n - --level=info\n")
deployPodinfo(h, "s43", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{
cmRef("podinfo-extraargs-base"),
cmRef("podinfo-extraargs-overlay"),
},
})
waitForReplicas(h, 1)
deploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
// podinfo 6.11.1 renders extraArgs into the container's .command
// (appended to ["./podinfo", "--port=...", ...]). Check both command
// and args to stay robust against chart layout changes.
c := deploy.Spec.Template.Spec.Containers[0]
joined := strings.Join(c.Command, " ") + " " + strings.Join(c.Args, " ")
Expect(joined).To(ContainSubstring("--level=info"),
"overlay array value must appear in the container's command/args")
Expect(joined).ToNot(ContainSubstring("--level=debug"),
"base array value must NOT appear — arrays are replaced not merged")
Expect(joined).ToNot(ContainSubstring("--timeout=30"),
"base orthogonal array item must NOT appear — arrays are replaced wholesale")
})
})
Context("valuesFrom combined with healthStatus criteria", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should reach healthy state using CM-supplied replicaCount", func() {
createCMWithReplicas(h, "podinfo-values", 2)
deployPodinfo(h, "s44", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{cmRef("podinfo-values")},
"healthStatus": []interface{}{
map[string]interface{}{
"resource": map[string]interface{}{"kind": "Deployment", "name": "podinfo"},
"condition": map[string]interface{}{"type": "Available"},
},
},
})
waitForReplicas(h, 2)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(h.ctx, h.appKey, h.app)).Should(Succeed())
g.Expect(h.app.Status.Services).ShouldNot(BeEmpty())
g.Expect(h.app.Status.Services[0].Healthy).Should(BeTrue())
}, 120*time.Second, 3*time.Second).Should(Succeed())
})
})
Context("Two helmchart components, each with its own valuesFrom source", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should render each component independently without cross-contamination", func() {
createCMWithReplicas(h, "podinfo-a-values", 2)
createSecretWithReplicas(h, "podinfo-b-values", 3)
compA := buildPodinfoComponent(h, "podinfo-a", "podinfo-a", map[string]interface{}{
"valuesFrom": []interface{}{cmRef("podinfo-a-values")},
})
compB := buildPodinfoComponent(h, "podinfo-b", "podinfo-b", map[string]interface{}{
"valuesFrom": []interface{}{secretRef("podinfo-b-values")},
})
deployAppWithComponents(h, "s45", []common2.ApplicationComponent{compA, compB})
waitForNamedReplicas(h, "podinfo-a", 2)
waitForNamedReplicas(h, "podinfo-b", 3)
})
})
Context("Self-healing restores a CM-backed Deployment after manual delete", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should initially deploy with CM-sourced replicaCount", func() {
createCMWithReplicas(h, "podinfo-values", 3)
deployPodinfo(h, "s46", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{cmRef("podinfo-values")},
})
waitForReplicas(h, 3)
})
It("should recreate the Deployment with the same CM values after it is deleted", func() {
runCommandSucceed("kubectl", "delete", "deployment", "podinfo", "-n", h.namespace)
Eventually(func() bool {
err := k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, &appsv1.Deployment{})
return err != nil
}, 10*time.Second, time.Second).Should(BeTrue())
RequestReconcileNow(h.ctx, h.app)
waitForReplicas(h, 3)
})
})
Context("Adoption of an existing vanilla Helm release with valuesFrom", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("should adopt the pre-existing release and merge CM values on the adoption upgrade", func() {
By("Installing podinfo via vanilla helm at replicaCount=1")
runCommandSucceed("helm", "install", "podinfo",
"--repo", "https://stefanprodan.github.io/podinfo", "podinfo",
"--version", "6.11.1", "--set", "replicaCount=1", "-n", h.namespace)
Eventually(func(g Gomega) {
d := &appsv1.Deployment{}
g.Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, d)).Should(Succeed())
g.Expect(d.Status.ReadyReplicas).Should(Equal(int32(1)))
}, 60*time.Second, 3*time.Second).Should(Succeed())
By("Creating CM with replicaCount=3 and applying the Application (adoption path)")
createCMWithReplicas(h, "podinfo-adopt-values", 3)
deployPodinfo(h, "s47", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{cmRef("podinfo-adopt-values")},
})
By("Verifying adoption applied CM values (replicas scaled 1→3) and injected KubeVela labels")
waitForReplicas(h, 3)
deploy := &appsv1.Deployment{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Namespace: h.namespace, Name: "podinfo"}, deploy)).Should(Succeed())
Expect(deploy.GetLabels()).To(HaveKey("app.oam.dev/name"),
"adoption must inject KubeVela ownership labels on the Deployment")
})
})
Context("Auto-reconcile on ConfigMap content change (without spec edit)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() { h.createNamespace() })
AfterAll(func() { h.cleanup() })
It("rolls out a new Helm revision when only the referenced ConfigMap changes", func() {
By("creating the backing ConfigMap with replicaCount=2 and deploying the Application")
createCMWithReplicas(h, "vf-autorec-values", 2)
deployPodinfo(h, "s48", "podinfo", map[string]interface{}{
"valuesFrom": []interface{}{cmRef("vf-autorec-values")},
})
waitForReplicas(h, 2)
By("recording the current Helm release secret count for later comparison")
initialCount := len(h.getHelmSecrets().Items)
By("editing the ConfigMap content (replicaCount: 2 -> 4) WITHOUT touching the Application spec")
cm := &corev1.ConfigMap{}
Expect(k8sClient.Get(h.ctx, types.NamespacedName{Name: "vf-autorec-values", Namespace: h.namespace}, cm)).Should(Succeed())
cm.Data["values.yaml"] = "replicaCount: 4\n"
Expect(k8sClient.Update(h.ctx, cm)).Should(Succeed())
By("forcing a reconcile via the requestreconcile annotation (skips the periodic-resync wait)")
RequestReconcileNow(h.ctx, h.app)
By("expecting the Deployment to roll forward to replicaCount=4 driven by the CM edit alone")
waitForReplicas(h, 4)
By("confirming a new Helm revision was created (release secret count grew)")
Eventually(func(g Gomega) {
secrets := h.getHelmSecrets()
g.Expect(len(secrets.Items)).Should(BeNumerically(">", initialCount))
}, 60*time.Second, 5*time.Second).Should(Succeed())
})
})
})
func init() {
// ensure helmchart test file is compiled and registered
_ = "helm chart tests registered"
}