Files
kubevela/test/e2e-test/helmchart_test.go

2589 lines
97 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", FlakeAttempts(2), 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("Waiting for Deployment to be ready before recording pod UIDs")
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(2)))
}, 120*time.Second, 3*time.Second).Should(Succeed())
By("Recording running pod UIDs before adoption")
var podList corev1.PodList
Expect(k8sClient.List(h.ctx, &podList, client.InNamespace(h.namespace),
client.MatchingLabels{"app.kubernetes.io/name": "podinfo"})).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", FlakeAttempts(2), 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)
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(2)))
}, 120*time.Second, 3*time.Second).Should(Succeed())
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)
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(2)))
}, 120*time.Second, 3*time.Second).Should(Succeed())
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", FlakeAttempts(2), 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())
})
})
})
// ============================================================================
// Helmchart Auth -- Secret-referenced authentication tests for GWCP-98771.
// All 19 scenarios assume the auth-test registries (deployed in BeforeSuite
// from test/e2e-test/testdata/auth/manifests/) are running and the test chart
// has been pushed to each.
// ============================================================================
var _ = Describe("Helmchart Auth", func() {
BeforeEach(func() {
if os.Getenv("KUBEVELA_E2E_AUTH") != "1" {
Skip("auth-test registries not deployed (set KUBEVELA_E2E_AUTH=1 to enable)")
}
})
const (
chartMuseumURL = "https://chartmuseum.kubevela-auth-test.svc.cluster.local:8080"
chartMuseumBearerURL = "https://chartmuseum-bearer.kubevela-auth-test.svc.cluster.local"
ociRegistrySource = "oci://zot.kubevela-auth-test.svc.cluster.local:5000/charts/podinfo"
ociRegistryHost = "zot.kubevela-auth-test.svc.cluster.local:5000"
authTestUser = "test-user"
authTestPass = "test-pass"
authBearerToken = "kubevela-auth-test-token"
)
// createSecretInline creates a Secret of the given type with stringData in the
// helmTestContext's namespace. Returns an error if creation fails; tests should
// Expect Succeed().
createSecretInline := func(h *helmTestContext, name string, secretType corev1.SecretType, stringData map[string]string) error {
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: h.namespace},
Type: secretType,
StringData: stringData,
}
return k8sClient.Create(h.ctx, s)
}
createSecretInNamespace := func(h *helmTestContext, name, ns string, secretType corev1.SecretType, stringData map[string]string) error {
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},
Type: secretType,
StringData: stringData,
}
return k8sClient.Create(h.ctx, s)
}
// chartWithRepoAuth builds a chart props map with a repo-based source and an auth secretRef.
chartWithRepoAuth := func(repoURL, secretName string, secretNs ...string) map[string]interface{} {
secretRefBlock := map[string]interface{}{"name": secretName}
if len(secretNs) > 0 && secretNs[0] != "" {
secretRefBlock["namespace"] = secretNs[0]
}
return map[string]interface{}{
"source": "podinfo",
"repoURL": repoURL,
"version": "1.0.0",
"auth": map[string]interface{}{"secretRef": secretRefBlock},
}
}
// chartWithURLAuth builds a chart props map with a direct .tgz URL and auth.
chartWithURLAuth := func(url, secretName string) map[string]interface{} {
return map[string]interface{}{
"source": url,
"version": "1.0.0",
"auth": map[string]interface{}{"secretRef": map[string]interface{}{"name": secretName}},
}
}
// chartWithOCIAuth builds a chart props map with an oci:// source and auth.
chartWithOCIAuth := func(secretName string) map[string]interface{} {
return map[string]interface{}{
"source": ociRegistrySource,
"version": "1.0.0",
"auth": map[string]interface{}{"secretRef": map[string]interface{}{"name": secretName}},
}
}
// buildPodinfoComponentForAuth is the auth-block analog of buildPodinfoComponent.
// It accepts a chart props map (assembled by chartWithRepoAuth/chartWithURLAuth/
// chartWithOCIAuth) and embeds it under "chart".
buildPodinfoComponentForAuth := func(h *helmTestContext, releaseName string, chartProps map[string]interface{}) common2.ApplicationComponent {
merged := map[string]interface{}{
"chart": chartProps,
"release": map[string]interface{}{
"name": releaseName,
"namespace": h.namespace,
},
"options": map[string]interface{}{
"createNamespace": true,
"skipTests": true,
},
}
raw, err := json.Marshal(merged)
Expect(err).ShouldNot(HaveOccurred())
return common2.ApplicationComponent{
Name: "podinfo",
Type: "helmchart",
Properties: &runtime.RawExtension{Raw: raw},
}
}
deployAuthAppSuccess := func(h *helmTestContext, prefix, releaseName string, chartProps map[string]interface{}) {
comp := buildPodinfoComponentForAuth(h, releaseName, chartProps)
h.app = &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: prefix + "-" + 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.Phase).Should(Equal(common2.ApplicationRunning))
}, 180*time.Second, 5*time.Second).Should(Succeed())
}
// deployAuthAppExpectFailure accepts either of two failure paths:
// 1) The validating webhook denies the Create() with the auth error in
// its message (the webhook does a dry-run render that exercises the
// resolver; resolver errors surface at admission time).
// 2) Create() succeeds and the workflow later reaches Phase="failed"
// with the error in a step message.
// Both are valid: the resolver runs in both contexts and surfaces the
// same verbatim, RFC-grounded message.
deployAuthAppExpectFailure := func(h *helmTestContext, prefix, releaseName string, chartProps map[string]interface{}, errSubstring string) {
comp := buildPodinfoComponentForAuth(h, releaseName, chartProps)
h.app = &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: prefix + "-" + rand.RandomString(4),
Namespace: h.appNamespace,
},
Spec: v1beta1.ApplicationSpec{Components: []common2.ApplicationComponent{comp}},
}
createErr := k8sClient.Create(h.ctx, h.app)
if createErr != nil {
Expect(createErr.Error()).To(ContainSubstring(errSubstring),
"webhook denial did not contain expected substring %q: %v", errSubstring, createErr)
return
}
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; workflow=%+v", errSubstring, h.app.Status.Workflow)
}, 180*time.Second, 5*time.Second).Should(Succeed())
}
// ----------- Positive paths ------------
// OCI plain-HTTP via Opaque Secret (insecurePlainHTTP opts in).
// The dispatcher branches for kubernetes.io/basic-auth and
// kubernetes.io/dockerconfigjson are exhaustively covered by
// auth_test.go unit tests; both produce the same HTTPOption shape
// the resolver returns for the Opaque (basic) path here, so an
// extra OCI e2e against typed Secrets is duplicative coverage.
Context("OCI / Opaque (basic) with insecurePlainHTTP", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("pulls and installs the chart over plain HTTP", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser,
"password": authTestPass,
"insecurePlainHTTP": "true",
})).To(Succeed())
deployAuthAppSuccess(h, "auth-oci-opaque", "podinfo", chartWithOCIAuth("creds"))
})
})
Context("HTTPS Helm-repo / kubernetes.io/basic-auth typed Secret", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("pulls and installs the chart", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeBasicAuth, map[string]string{
"username": authTestUser, "password": authTestPass,
})).To(Succeed())
deployAuthAppSuccess(h, "auth-http-basic-typed", "podinfo", chartWithRepoAuth(chartMuseumURL, "creds"))
})
})
Context("HTTPS Helm-repo / Opaque (basic)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("pulls and installs the chart", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser, "password": authTestPass,
})).To(Succeed())
deployAuthAppSuccess(h, "auth-http-basic-opaque", "podinfo", chartWithRepoAuth(chartMuseumURL, "creds"))
})
})
Context("HTTPS Helm-repo / Bearer token via nginx-fronted ChartMuseum", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("pulls and installs the chart with Authorization: Bearer", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"token": authBearerToken,
})).To(Succeed())
deployAuthAppSuccess(h, "auth-http-bearer", "podinfo", chartWithRepoAuth(chartMuseumBearerURL, "creds"))
})
})
Context("HTTPS Helm-repo / Opaque (basic + insecureSkipTLS)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("pulls and installs the chart with TLS verification disabled", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser, "password": authTestPass, "insecureSkipTLS": "true",
})).To(Succeed())
deployAuthAppSuccess(h, "auth-http-skip-tls", "podinfo", chartWithRepoAuth(chartMuseumURL, "creds"))
})
})
Context("URL direct .tgz / Opaque (basic)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("pulls and installs the chart from a direct .tgz URL", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser, "password": authTestPass,
})).To(Succeed())
url := chartMuseumURL + "/charts/podinfo-1.0.0.tgz"
deployAuthAppSuccess(h, "auth-url-direct", "podinfo", chartWithURLAuth(url, "creds"))
})
})
Context("secretRef.namespace omitted (defaults to release namespace)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("resolves the Secret from the release namespace", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser, "password": authTestPass,
})).To(Succeed())
// No secretRef.namespace; resolver defaults to release namespace.
deployAuthAppSuccess(h, "auth-ns-omitted", "podinfo", chartWithRepoAuth(chartMuseumURL, "creds"))
})
})
Context("secretRef.namespace explicitly set to Application namespace", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("resolves the Secret when secretRef.namespace == Application namespace", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser, "password": authTestPass,
})).To(Succeed())
deployAuthAppSuccess(h, "auth-ns-app", "podinfo", chartWithRepoAuth(chartMuseumURL, "creds", h.namespace))
})
})
// ----------- Negative paths ------------
Context("Missing Secret", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("fails with the not-found error", func() {
deployAuthAppExpectFailure(h, "auth-missing", "podinfo",
chartWithRepoAuth(chartMuseumURL, "nonexistent-secret"),
`not found: it MUST exist in the release namespace`)
})
})
Context("Wrong Secret type", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("rejects an unsupported Secret type", func() {
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{Name: "dummy-sa", Namespace: h.namespace},
}
Expect(k8sClient.Create(h.ctx, sa)).To(Succeed())
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "creds", Namespace: h.namespace,
Annotations: map[string]string{"kubernetes.io/service-account.name": "dummy-sa"},
},
Type: corev1.SecretTypeServiceAccountToken,
}
Expect(k8sClient.Create(h.ctx, s)).To(Succeed())
deployAuthAppExpectFailure(h, "auth-wrong-type", "podinfo",
chartWithRepoAuth(chartMuseumURL, "creds"),
`MUST be one of kubernetes.io/basic-auth, kubernetes.io/dockerconfigjson, kubernetes.io/tls, or Opaque`)
})
})
Context("Cross-namespace Secret rejected", Ordered, func() {
h := newHelmTestContext()
otherNS := ""
BeforeAll(func() {
h.createNamespace()
otherNS = "auth-cross-ns-other-" + rand.RandomString(4)
Expect(k8sClient.Create(h.ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: otherNS}})).To(Succeed())
Expect(createSecretInNamespace(h, "creds", otherNS, corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser, "password": authTestPass,
})).To(Succeed())
})
AfterAll(func() {
_ = k8sClient.Delete(h.ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: otherNS}})
h.cleanup()
})
It("rejects a Secret reference outside release-ns and app-ns", func() {
deployAuthAppExpectFailure(h, "auth-cross-ns", "podinfo",
chartWithRepoAuth(chartMuseumURL, "creds", otherNS),
`namespace MUST equal the release namespace`)
})
})
Context("Opaque mixed credentials (basic + token)", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("rejects Opaque Secret with both basic-auth keys and a token", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": authTestUser, "password": authTestPass, "token": "some.bearer.token",
})).To(Succeed())
deployAuthAppExpectFailure(h, "auth-mixed", "podinfo",
chartWithRepoAuth(chartMuseumURL, "creds"),
`at most one credential method MUST be configured per Secret (RFC 6750 §2)`)
})
})
Context("Bearer token over plain http://", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("rejects Bearer over plain HTTP per RFC 6750 §2", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"token": authBearerToken,
})).To(Succeed())
deployAuthAppExpectFailure(h, "auth-bearer-http", "podinfo",
chartWithRepoAuth("http://chartmuseum.kubevela-auth-test.svc.cluster.local:8080", "creds"),
`bearer tokens MUST be sent only over HTTPS or OCI (RFC 6750 §2)`)
})
})
Context("Bearer token + insecureSkipTLS", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("rejects Bearer combined with TLS verification disabled per RFC 6750 §2", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"token": authBearerToken, "insecureSkipTLS": "true",
})).To(Succeed())
deployAuthAppExpectFailure(h, "auth-bearer-insecure", "podinfo",
chartWithRepoAuth(chartMuseumURL, "creds"),
`bearer tokens MUST NOT be sent with TLS verification disabled (RFC 6750 §2)`)
})
})
Context("User-supplied Bearer on OCI source", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("rejects user-supplied Bearer on OCI sources", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"token": authBearerToken,
})).To(Succeed())
deployAuthAppExpectFailure(h, "auth-bearer-oci", "podinfo",
chartWithOCIAuth("creds"),
`user-supplied bearer tokens MUST NOT be used with OCI sources`)
})
})
Context("Username containing ':'", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("rejects per RFC 7617 §2", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"username": "user:colon", "password": authTestPass,
})).To(Succeed())
deployAuthAppExpectFailure(h, "auth-colon", "podinfo",
chartWithRepoAuth(chartMuseumURL, "creds"),
`username MUST NOT contain ':' (RFC 7617 §2)`)
})
})
Context("Bearer token charset violation", Ordered, func() {
h := newHelmTestContext()
BeforeAll(func() {
h.createNamespace()
})
AfterAll(func() {
h.cleanup()
})
It("rejects per RFC 6750 §2.1", func() {
Expect(createSecretInline(h, "creds", corev1.SecretTypeOpaque, map[string]string{
"token": "bad token with spaces",
})).To(Succeed())
deployAuthAppExpectFailure(h, "auth-token-charset", "podinfo",
chartWithRepoAuth(chartMuseumBearerURL, "creds"),
`b64token charset (RFC 6750 §2.1)`)
})
})
})
func init() {
// ensure helmchart test file is compiled and registered
_ = "helm chart tests registered"
}