diff --git a/README.md b/README.md
index 269412d6..6339ee3d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-
+
diff --git a/api/v1alpha1/helmchartproxy_types.go b/api/v1alpha1/helmchartproxy_types.go
index 6ecfda69..c6c4c0c8 100644
--- a/api/v1alpha1/helmchartproxy_types.go
+++ b/api/v1alpha1/helmchartproxy_types.go
@@ -84,6 +84,9 @@ type HelmChartProxySpec struct {
// +optional
ReconcileStrategy string `json:"reconcileStrategy,omitempty"`
+ // TODO (dmvolod) Add notes about release drift description and warning
+ ReleaseDrift bool `json:"releaseDrift,omitempty"`
+
// Options represents CLI flags passed to Helm operations (i.e. install, upgrade, delete) and
// include options such as wait, skipCRDs, timeout, waitForJobs, etc.
// +optional
diff --git a/api/v1alpha1/helmreleaseproxy_types.go b/api/v1alpha1/helmreleaseproxy_types.go
index 1ec60c9f..76ffa300 100644
--- a/api/v1alpha1/helmreleaseproxy_types.go
+++ b/api/v1alpha1/helmreleaseproxy_types.go
@@ -78,6 +78,8 @@ type HelmReleaseProxySpec struct {
// +optional
ReconcileStrategy string `json:"reconcileStrategy,omitempty"`
+ ReleaseDrift bool `json:"releaseDrift,omitempty"`
+
// Options represents the helm setting options which can be used to control behaviour of helm operations(Install, Upgrade, Delete, etc)
// via options like wait, skipCrds, timeout, waitForJobs, etc.
// +optional
diff --git a/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml b/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml
index 7b7b30b3..b8c987b2 100644
--- a/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml
+++ b/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml
@@ -270,6 +270,8 @@ spec:
- InstallOnce
- Continuous
type: string
+ releaseDrift:
+ type: boolean
releaseName:
description: ReleaseName is the release name of the installed Helm
chart. If it is not specified, a name will be generated.
diff --git a/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml b/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml
index 08094ac7..905a653b 100644
--- a/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml
+++ b/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml
@@ -276,6 +276,8 @@ spec:
- InstallOnce
- Continuous
type: string
+ releaseDrift:
+ type: boolean
releaseName:
description: ReleaseName is the release name of the installed Helm
chart. If it is not specified, a name will be generated.
diff --git a/controllers/helmchartproxy/helmchartproxy_controller_phases.go b/controllers/helmchartproxy/helmchartproxy_controller_phases.go
index ac8edafb..dab47201 100644
--- a/controllers/helmchartproxy/helmchartproxy_controller_phases.go
+++ b/controllers/helmchartproxy/helmchartproxy_controller_phases.go
@@ -233,6 +233,7 @@ func constructHelmReleaseProxy(existing *addonsv1alpha1.HelmReleaseProxy, helmCh
}
helmReleaseProxy.Spec.ReconcileStrategy = helmChartProxy.Spec.ReconcileStrategy
+ helmReleaseProxy.Spec.ReleaseDrift = helmChartProxy.Spec.ReleaseDrift
helmReleaseProxy.Spec.Version = helmChartProxy.Spec.Version
helmReleaseProxy.Spec.Values = parsedValues
helmReleaseProxy.Spec.Options = helmChartProxy.Spec.Options
@@ -288,6 +289,9 @@ func shouldReinstallHelmRelease(ctx context.Context, existing *addonsv1alpha1.He
case existing.Spec.ReleaseNamespace != helmChartProxy.Spec.ReleaseNamespace:
log.V(2).Info("ReleaseNamespace changed", "existing", existing.Spec.ReleaseNamespace, "helmChartProxy", helmChartProxy.Spec.ReleaseNamespace)
return true
+ case existing.Spec.ReleaseDrift != helmChartProxy.Spec.ReleaseDrift:
+ log.V(2).Info("ReleaseDrift changed", "existing", existing.Spec.ReleaseDrift, "helmChartProxy", helmChartProxy.Spec.ReleaseDrift)
+ return true
}
return false
diff --git a/controllers/helmreleasedrift/manager.go b/controllers/helmreleasedrift/manager.go
new file mode 100644
index 00000000..7f7048d4
--- /dev/null
+++ b/controllers/helmreleasedrift/manager.go
@@ -0,0 +1,146 @@
+/*
+Copyright 2022 The Kubernetes 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 helmreleasedrift
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/ironcore-dev/controller-utils/unstructuredutils"
+ "golang.org/x/exp/slices"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/cache"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+ "sigs.k8s.io/kustomize/api/konfig"
+)
+
+const (
+ InstanceLabelKey = "app.kubernetes.io/instance"
+ ManagedByLabelValue = "Helm"
+)
+
+var (
+ managers = map[string]options{}
+ mutex sync.Mutex
+)
+
+type options struct {
+ gvks []schema.GroupVersionKind
+ cancel context.CancelFunc
+}
+
+func Add(ctx context.Context, restConfig *rest.Config, helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy, releaseManifest string, eventChannel chan event.GenericEvent) error {
+ log := ctrl.LoggerFrom(ctx)
+ gvks, err := extractGVKsFromManifest(releaseManifest)
+ if err != nil {
+ return err
+ }
+
+ manager, exist := managers[managerKey(helmReleaseProxy)]
+ if exist {
+ if slices.Equal(manager.gvks, gvks) {
+ return nil
+ }
+ Remove(helmReleaseProxy)
+ }
+
+ mutex.Lock()
+ defer mutex.Unlock()
+ k8sManager, err := ctrl.NewManager(restConfig, ctrl.Options{
+ Scheme: scheme.Scheme,
+ Metrics: metricsserver.Options{
+ BindAddress: "0",
+ },
+ HealthProbeBindAddress: "0",
+ Cache: cache.Options{
+ DefaultLabelSelector: labels.SelectorFromSet(map[string]string{
+ konfig.ManagedbyLabelKey: ManagedByLabelValue,
+ InstanceLabelKey: helmReleaseProxy.Spec.ReleaseName,
+ }),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if err = (&releaseDriftReconciler{
+ Client: k8sManager.GetClient(),
+ Scheme: k8sManager.GetScheme(),
+ HelmReleaseProxyKey: client.ObjectKeyFromObject(helmReleaseProxy),
+ HelmReleaseProxyEvent: eventChannel,
+ }).setupWithManager(k8sManager, gvks); err != nil {
+ return err
+ }
+ log.V(2).Info("Starting release drift controller manager")
+ ctx, cancel := context.WithCancel(ctx)
+ go func() {
+ if err = k8sManager.Start(ctx); err != nil {
+ log.V(2).Error(err, "failed to start release drift manager")
+ objectMeta := metav1.ObjectMeta{
+ Name: helmReleaseProxy.Name,
+ Namespace: helmReleaseProxy.Namespace,
+ }
+ eventChannel <- event.GenericEvent{Object: &addonsv1alpha1.HelmReleaseProxy{ObjectMeta: objectMeta}}
+ }
+ }()
+
+ managers[managerKey(helmReleaseProxy)] = options{
+ gvks: gvks,
+ cancel: cancel,
+ }
+
+ return nil
+}
+
+func Remove(helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy) {
+ mutex.Lock()
+ defer mutex.Unlock()
+
+ manager, exist := managers[managerKey(helmReleaseProxy)]
+ if exist {
+ manager.cancel()
+ delete(managers, managerKey(helmReleaseProxy))
+ }
+}
+
+func managerKey(helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy) string {
+ return fmt.Sprintf("%s-%s-%s", helmReleaseProxy.Spec.ClusterRef.Name, helmReleaseProxy.Namespace, helmReleaseProxy.Spec.ReleaseName)
+}
+
+func extractGVKsFromManifest(manifest string) ([]schema.GroupVersionKind, error) {
+ objects, err := unstructuredutils.Read(strings.NewReader(manifest))
+ if err != nil {
+ return nil, err
+ }
+ var gvks []schema.GroupVersionKind
+ for _, obj := range objects {
+ if !slices.Contains(gvks, obj.GroupVersionKind()) {
+ gvks = append(gvks, obj.GroupVersionKind())
+ }
+ }
+
+ return gvks, nil
+}
diff --git a/controllers/helmreleasedrift/manager_test.go b/controllers/helmreleasedrift/manager_test.go
new file mode 100644
index 00000000..904f8181
--- /dev/null
+++ b/controllers/helmreleasedrift/manager_test.go
@@ -0,0 +1,93 @@
+/*
+Copyright 2022 The Kubernetes 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 helmreleasedrift_test
+
+import (
+ "github.com/ironcore-dev/controller-utils/metautils"
+ . "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/utils/ptr"
+ addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
+ "sigs.k8s.io/cluster-api-addon-provider-helm/controllers/helmreleasedrift"
+ "sigs.k8s.io/cluster-api-addon-provider-helm/controllers/helmreleasedrift/test/fake"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+)
+
+const (
+ releaseName = "ahoy"
+ objectName = "ahoy-hello-world"
+ originalDeploymentReplicas = 1
+ patchedDeploymentReplicas = 3
+)
+
+var _ = Describe("Testing HelmReleaseProxy drift manager with fake manifest", func() {
+ It("Adding HelmReleaseProxy drift manager and validating its lifecycle", func() {
+ objectMeta := metav1.ObjectMeta{
+ Name: releaseName,
+ Namespace: metav1.NamespaceDefault,
+ }
+ fake.ManifestEventChannel <- event.GenericEvent{Object: &addonsv1alpha1.HelmReleaseProxy{ObjectMeta: objectMeta}}
+
+ helmReleaseProxy := &addonsv1alpha1.HelmReleaseProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ahoy-release-proxy",
+ Namespace: metav1.NamespaceDefault,
+ },
+ Spec: addonsv1alpha1.HelmReleaseProxySpec{
+ ReleaseName: releaseName,
+ },
+ }
+
+ // TODO (dvolodin) Find way how to wait manager to start for testing
+ err := helmreleasedrift.Add(ctx, restConfig, helmReleaseProxy, manifest, fake.ManifestEventChannel)
+ Expect(err).NotTo(HaveOccurred())
+
+ Eventually(func() bool {
+ for _, objectList := range []client.ObjectList{&corev1.ServiceList{}, &appsv1.DeploymentList{}, &corev1.ServiceAccountList{}} {
+ err := k8sClient.List(ctx, objectList, client.InNamespace(metav1.NamespaceDefault), client.MatchingLabels(map[string]string{helmreleasedrift.InstanceLabelKey: releaseName}))
+ if err != nil {
+ return false
+ }
+ objects, err := metautils.ExtractList(objectList)
+ if err != nil || len(objects) == 0 {
+ return false
+ }
+ }
+
+ return true
+ }, timeout, interval).Should(BeTrue())
+
+ deployment := &appsv1.Deployment{}
+ err = k8sClient.Get(ctx, client.ObjectKey{Name: objectName, Namespace: metav1.NamespaceDefault}, deployment)
+ Expect(err).NotTo(HaveOccurred())
+ patch := client.MergeFrom(deployment.DeepCopy())
+ deployment.Spec.Replicas = ptr.To(int32(patchedDeploymentReplicas))
+ err = k8sClient.Patch(ctx, deployment, patch)
+ Expect(err).NotTo(HaveOccurred())
+
+ Eventually(func() bool {
+ err = k8sClient.Get(ctx, client.ObjectKey{Name: objectName, Namespace: metav1.NamespaceDefault}, deployment)
+ return err == nil && *deployment.Spec.Replicas == originalDeploymentReplicas
+ }, timeout, interval).Should(BeTrue())
+
+ helmreleasedrift.Remove(helmReleaseProxy)
+ })
+})
diff --git a/controllers/helmreleasedrift/releasedrift_controller.go b/controllers/helmreleasedrift/releasedrift_controller.go
new file mode 100644
index 00000000..818420cd
--- /dev/null
+++ b/controllers/helmreleasedrift/releasedrift_controller.go
@@ -0,0 +1,98 @@
+/*
+Copyright 2022 The Kubernetes 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 helmreleasedrift
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+)
+
+// releaseDriftReconciler reconciles an event from the all helm objects managed by the HelmReleaseProxy.
+type releaseDriftReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ HelmReleaseProxyKey client.ObjectKey
+ HelmReleaseProxyEvent chan event.GenericEvent
+}
+
+var excludeCreateEventsPredicate = predicate.Funcs{
+ CreateFunc: func(e event.CreateEvent) bool {
+ return false
+ },
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ return shouldFilteredByManager(e.ObjectNew.GetManagedFields())
+
+ },
+ DeleteFunc: func(e event.DeleteEvent) bool {
+ return shouldFilteredByManager(e.Object.GetManagedFields())
+ },
+}
+
+func shouldFilteredByManager(mfs []metav1.ManagedFieldsEntry) bool {
+ mfl := len(mfs)
+ if mfl > 0 {
+ manager := mfs[mfl-1].Manager
+ return !(manager == os.Args[0])
+ }
+
+ return false
+}
+
+// setupWithManager sets up the controller with the Manager.
+func (r *releaseDriftReconciler) setupWithManager(mgr ctrl.Manager, gvks []schema.GroupVersionKind) error {
+ controllerBuilder := ctrl.NewControllerManagedBy(mgr).
+ Named(fmt.Sprintf("%s-%s-release-drift-controller", r.HelmReleaseProxyKey.Name, r.HelmReleaseProxyKey.Namespace))
+ for _, gvk := range gvks {
+ watch := &unstructured.Unstructured{}
+ watch.SetGroupVersionKind(gvk)
+ controllerBuilder.Watches(watch, handler.EnqueueRequestsFromMapFunc(r.WatchesToReleaseMapper), builder.OnlyMetadata)
+ }
+
+ return controllerBuilder.WithEventFilter(excludeCreateEventsPredicate).Complete(r)
+}
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+func (r *releaseDriftReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
+ log := ctrl.LoggerFrom(ctx)
+ log.V(2).Info("Beginning reconciliation", "requestNamespace", req.Namespace, "requestName", req.Name)
+
+ objectMeta := metav1.ObjectMeta{
+ Name: r.HelmReleaseProxyKey.Name,
+ Namespace: r.HelmReleaseProxyKey.Namespace,
+ }
+ r.HelmReleaseProxyEvent <- event.GenericEvent{Object: &addonsv1alpha1.HelmReleaseProxy{ObjectMeta: objectMeta}}
+
+ return ctrl.Result{}, nil
+}
+
+func (r *releaseDriftReconciler) WatchesToReleaseMapper(_ context.Context, _ client.Object) []ctrl.Request {
+ return []ctrl.Request{{NamespacedName: r.HelmReleaseProxyKey}}
+}
diff --git a/controllers/helmreleasedrift/suite_test.go b/controllers/helmreleasedrift/suite_test.go
new file mode 100644
index 00000000..1be3403e
--- /dev/null
+++ b/controllers/helmreleasedrift/suite_test.go
@@ -0,0 +1,128 @@
+/*
+Copyright 2022 The Kubernetes 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 helmreleasedrift_test
+
+import (
+ "context"
+ _ "embed"
+ "flag"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/ironcore-dev/controller-utils/unstructuredutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ "k8s.io/klog/v2"
+ "k8s.io/klog/v2/textlogger"
+ "sigs.k8s.io/cluster-api-addon-provider-helm/controllers/helmreleasedrift/test/fake"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ "sigs.k8s.io/controller-runtime/pkg/manager"
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var (
+ k8sClient client.Client
+ testEnv *envtest.Environment
+ k8sManager manager.Manager
+ ctx context.Context
+ cancel context.CancelFunc
+ restConfig *rest.Config
+)
+
+//go:embed testdata/manifest.yaml
+var manifest string
+
+const (
+ timeout = time.Second * 10
+ interval = time.Millisecond * 250
+)
+
+func TestDriftManager(t *testing.T) {
+ RegisterFailHandler(Fail)
+
+ RunSpecs(t, "Drift Manager Suite")
+}
+
+var _ = BeforeSuite(func() {
+ ctx, cancel = context.WithCancel(context.TODO())
+
+ fs := flag.FlagSet{}
+ klog.InitFlags(&fs)
+ err := fs.Set("v", "2")
+ Expect(err).NotTo(HaveOccurred())
+ ctrl.SetLogger(textlogger.NewLogger(textlogger.NewConfig()))
+
+ By("bootstrapping test environment")
+ testEnv = &envtest.Environment{
+ CRDDirectoryPaths: []string{
+ filepath.Join("..", "..", "config", "crd", "bases"),
+ },
+ ErrorIfCRDPathMissing: true,
+ }
+
+ restConfig, err = testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(restConfig).NotTo(BeNil())
+ //+kubebuilder:scaffold:scheme
+
+ k8sClient, err = client.New(restConfig, client.Options{Scheme: scheme.Scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+
+ k8sManager, err = ctrl.NewManager(restConfig, ctrl.Options{
+ Scheme: scheme.Scheme,
+ Metrics: metricsserver.Options{
+ BindAddress: "0",
+ },
+ HealthProbeBindAddress: "0",
+ })
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+
+ manifest, err := unstructuredutils.Read(strings.NewReader(manifest))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(manifest).NotTo(BeNil())
+
+ err = (&fake.ManifestReconciler{
+ Client: k8sManager.GetClient(),
+ Scheme: k8sManager.GetScheme(),
+ ManifestObjects: unstructuredutils.UnstructuredSliceToObjectSlice(manifest),
+ }).SetupWithManager(k8sManager)
+ Expect(err).ToNot(HaveOccurred())
+
+ go func() {
+ defer GinkgoRecover()
+ err = k8sManager.Start(ctx)
+ Expect(err).ToNot(HaveOccurred(), "failed to run manager")
+ }()
+})
+
+var _ = AfterSuite(func() {
+ cancel()
+ By("tearing down the test environment")
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
diff --git a/controllers/helmreleasedrift/test/fake/manifest_controller.go b/controllers/helmreleasedrift/test/fake/manifest_controller.go
new file mode 100644
index 00000000..7e9a4b91
--- /dev/null
+++ b/controllers/helmreleasedrift/test/fake/manifest_controller.go
@@ -0,0 +1,74 @@
+/*
+Copyright 2022 The Kubernetes 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 fake
+
+import (
+ "context"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/source"
+)
+
+// ManifestReconciler reconciles an event from all fake manifest objects channel.
+type ManifestReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ ManifestObjects []client.Object
+}
+
+var ManifestEventChannel = make(chan event.GenericEvent, 1)
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *ManifestReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewControllerManagedBy(mgr).
+ Named("fake-manifest-controller").
+ WatchesRawSource(source.Channel(ManifestEventChannel, &handler.EnqueueRequestForObject{})).
+ Complete(r)
+}
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
+ log := ctrl.LoggerFrom(ctx)
+ log.V(2).Info("Beginning reconciliation", "requestNamespace", req.Namespace, "requestName", req.Name)
+
+ for _, object := range r.ManifestObjects {
+ object.SetNamespace(req.Namespace)
+ requestObject, _ := object.DeepCopyObject().(client.Object)
+ err := r.Get(ctx, client.ObjectKeyFromObject(object), requestObject)
+ if client.IgnoreNotFound(err) != nil {
+ return ctrl.Result{}, err
+ }
+ if apierrors.IsNotFound(err) {
+ if err = r.Client.Create(ctx, requestObject); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ continue
+ }
+ if err = r.Client.Update(ctx, object); err != nil {
+ return ctrl.Result{}, err
+ }
+ }
+
+ return ctrl.Result{}, nil
+}
diff --git a/controllers/helmreleasedrift/testdata/manifest.yaml b/controllers/helmreleasedrift/testdata/manifest.yaml
new file mode 100644
index 00000000..84095605
--- /dev/null
+++ b/controllers/helmreleasedrift/testdata/manifest.yaml
@@ -0,0 +1,75 @@
+---
+# Source: hello-world/templates/serviceaccount.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: ahoy-hello-world
+ labels:
+ helm.sh/chart: hello-world-0.1.0
+ app.kubernetes.io/name: hello-world
+ app.kubernetes.io/instance: ahoy
+ app.kubernetes.io/version: "1.16.0"
+ app.kubernetes.io/managed-by: Helm
+---
+# Source: hello-world/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: ahoy-hello-world
+ labels:
+ helm.sh/chart: hello-world-0.1.0
+ app.kubernetes.io/name: hello-world
+ app.kubernetes.io/instance: ahoy
+ app.kubernetes.io/version: "1.16.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: hello-world
+ app.kubernetes.io/instance: ahoy
+---
+# Source: hello-world/templates/deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: ahoy-hello-world
+ labels:
+ helm.sh/chart: hello-world-0.1.0
+ app.kubernetes.io/name: hello-world
+ app.kubernetes.io/instance: ahoy
+ app.kubernetes.io/version: "1.16.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: hello-world
+ app.kubernetes.io/instance: ahoy
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: hello-world
+ app.kubernetes.io/instance: ahoy
+ spec:
+ serviceAccountName: ahoy-hello-world
+ containers:
+ - name: hello-world
+ image: "nginx:1.16.0"
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: http
+ containerPort: 80
+ protocol: TCP
+ livenessProbe:
+ httpGet:
+ path: /
+ port: http
+ readinessProbe:
+ httpGet:
+ path: /
+ port: http
diff --git a/controllers/helmreleaseproxy/helmreleaseproxy_controller.go b/controllers/helmreleaseproxy/helmreleaseproxy_controller.go
index 07f66ad4..b02dace0 100644
--- a/controllers/helmreleaseproxy/helmreleaseproxy_controller.go
+++ b/controllers/helmreleaseproxy/helmreleaseproxy_controller.go
@@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
+ "sigs.k8s.io/cluster-api-addon-provider-helm/controllers/helmreleasedrift"
"sigs.k8s.io/cluster-api-addon-provider-helm/internal"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/controllers/remote"
@@ -42,7 +43,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/source"
)
// HelmReleaseProxyReconciler reconciles a HelmReleaseProxy object.
@@ -55,6 +58,8 @@ type HelmReleaseProxyReconciler struct {
WatchFilterValue string
}
+var helmReleaseProxyEventChannel = make(chan event.GenericEvent)
+
// SetupWithManager sets up the controller with the Manager.
func (r *HelmReleaseProxyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
log := ctrl.LoggerFrom(ctx)
@@ -80,6 +85,7 @@ func (r *HelmReleaseProxyReconciler) SetupWithManager(ctx context.Context, mgr c
predicates.ResourceHasFilterLabel(mgr.GetScheme(), ctrl.LoggerFrom(ctx), r.WatchFilterValue),
),
)).
+ WatchesRawSource(source.Channel(helmReleaseProxyEventChannel, &handler.EnqueueRequestForObject{})).
Complete(r)
}
@@ -299,6 +305,11 @@ func (r *HelmReleaseProxyReconciler) reconcileNormal(ctx context.Context, helmRe
conditions.MarkTrue(helmReleaseProxy, addonsv1alpha1.HelmReleaseReadyCondition)
annotations[addonsv1alpha1.ReleaseSuccessfullyInstalledAnnotation] = "true"
helmReleaseProxy.SetAnnotations(annotations)
+ if helmReleaseProxy.Spec.ReleaseDrift {
+ if err = helmreleasedrift.Add(ctx, restConfig, helmReleaseProxy, release.Manifest, helmReleaseProxyEventChannel); err != nil {
+ return err
+ }
+ }
case status.IsPending():
conditions.MarkFalse(helmReleaseProxy, addonsv1alpha1.HelmReleaseReadyCondition, addonsv1alpha1.HelmReleasePendingReason, clusterv1.ConditionSeverityInfo, "Helm release is in a pending state: %s", status)
case status == helmRelease.StatusFailed && err == nil:
@@ -351,6 +362,9 @@ func (r *HelmReleaseProxyReconciler) reconcileDelete(ctx context.Context, helmRe
log.V(2).Info(fmt.Sprintf("Chart '%s' successfully uninstalled on cluster %s", helmReleaseProxy.Spec.ChartName, helmReleaseProxy.Spec.ClusterRef.Name))
conditions.MarkFalse(helmReleaseProxy, addonsv1alpha1.HelmReleaseReadyCondition, addonsv1alpha1.HelmReleaseDeletedReason, clusterv1.ConditionSeverityInfo, "")
+ if helmReleaseProxy.Spec.ReleaseDrift {
+ helmreleasedrift.Remove(helmReleaseProxy)
+ }
if response != nil && response.Info != "" {
log.V(2).Info(fmt.Sprintf("Response is %s", response.Info))
}
@@ -369,7 +383,7 @@ func initializeConditions(ctx context.Context, patchHelper *patch.Helper, helmRe
}
// patchHelmReleaseProxy patches the HelmReleaseProxy object and sets the ReadyCondition as an aggregate of the other condition set.
-// TODO: Is this preferrable to client.Update() calls? Based on testing it seems like it avoids race conditions.
+// TODO: Is this preferable to client.Update() calls? Based on testing it seems like it avoids race conditions.
func patchHelmReleaseProxy(ctx context.Context, patchHelper *patch.Helper, helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy) error {
conditions.SetSummary(helmReleaseProxy,
conditions.WithConditions(
@@ -493,7 +507,7 @@ func (r *HelmReleaseProxyReconciler) getCACertificateFromSecret(ctx context.Cont
// writeCACertificateToFile writes the CA certificate to a temporary file.
func writeCACertificateToFile(ctx context.Context, caCertificate []byte) (string, error) {
log := ctrl.LoggerFrom(ctx)
- log.V(2).Info("Writing CA certficate to file")
+ log.V(2).Info("Writing CA certificate to file")
caCertFile, err := os.CreateTemp("", "ca-*.crt")
if err != nil {
return "", err
diff --git a/go.mod b/go.mod
index 5da8e8dd..d035295d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,20 @@
module sigs.k8s.io/cluster-api-addon-provider-helm
-go 1.22.0
+go 1.22.3
toolchain go1.22.10
require (
github.com/Masterminds/sprig/v3 v3.3.0
+ github.com/databus23/helm-diff/v3 v3.0.0-00010101000000-000000000000
github.com/google/go-cmp v0.6.0
+ github.com/ironcore-dev/controller-utils v0.9.4
github.com/onsi/ginkgo/v2 v2.22.2
github.com/onsi/gomega v1.36.2
github.com/pkg/errors v0.9.1
github.com/spf13/pflag v1.0.5
go.uber.org/mock v0.5.0
+ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.15.4
k8s.io/api v0.31.3
@@ -24,6 +27,8 @@ require (
sigs.k8s.io/cluster-api v1.9.3
sigs.k8s.io/cluster-api/test v1.9.3
sigs.k8s.io/controller-runtime v0.19.3
+ sigs.k8s.io/kustomize/api v0.17.3
+ sigs.k8s.io/kustomize/kyaml v0.17.2
)
require (
@@ -42,6 +47,7 @@ require (
github.com/adrg/xdg v0.5.3 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
@@ -63,7 +69,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
- github.com/evanphx/json-patch v5.7.0+incompatible // indirect
+ github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/color v1.18.0 // indirect
@@ -84,6 +90,12 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
+ github.com/gonvenience/bunt v1.3.5 // indirect
+ github.com/gonvenience/neat v1.3.13 // indirect
+ github.com/gonvenience/term v1.0.2 // indirect
+ github.com/gonvenience/text v1.0.7 // indirect
+ github.com/gonvenience/wrap v1.2.0 // indirect
+ github.com/gonvenience/ytbx v1.4.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
@@ -102,6 +114,7 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/homeport/dyff v1.9.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -113,13 +126,18 @@ require (
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.8 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
+ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/mitchellh/hashstructure v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -145,6 +163,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -154,7 +173,9 @@ require (
github.com/spf13/viper v1.19.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/texttheater/golang-levenshtein v1.0.1 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
+ github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
@@ -172,7 +193,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
- golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
@@ -200,10 +220,11 @@ require (
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kind v0.25.0 // indirect
- sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
- sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
-replace sigs.k8s.io/cluster-api => sigs.k8s.io/cluster-api v1.9.3
+replace (
+ github.com/databus23/helm-diff/v3 => github.com/dmvolod/helm-diff/v3 v3.9.10-fix.2
+ sigs.k8s.io/cluster-api => sigs.k8s.io/cluster-api v1.9.3
+)
diff --git a/go.sum b/go.sum
index 763b8104..33ddc8bd 100644
--- a/go.sum
+++ b/go.sum
@@ -40,6 +40,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -102,6 +104,8 @@ github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aB
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/dmvolod/helm-diff/v3 v3.9.10-fix.2 h1:z0X2H57YK3DrYhFIbs+nLWDHy4eBgyIKOZ/qU4VObWA=
+github.com/dmvolod/helm-diff/v3 v3.9.10-fix.2/go.mod h1:d/Osu8Gi0QVeEo/+9VGrzpdAv+F2l3Xi0G/8PgZf+o0=
github.com/docker/cli v25.0.5+incompatible h1:3Llw3kcE1gOScEojA247iDD+p1l9hHeC7H3vf3Zd5fk=
github.com/docker/cli v25.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
@@ -128,8 +132,8 @@ github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtz
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
-github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
+github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
@@ -204,6 +208,18 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
+github.com/gonvenience/bunt v1.3.5 h1:wSQquifvwEWtzn27k1ngLfeLaStyt0k1b/K6TrlCNAs=
+github.com/gonvenience/bunt v1.3.5/go.mod h1:7ApqkVBEWvX04oJ28Q2WeI/BvJM6VtukaJAU/q/pTs8=
+github.com/gonvenience/neat v1.3.13 h1:wRp1k0GX5EOpelNH3GyLaFy4SvnJ6k1U5SenmEWkXko=
+github.com/gonvenience/neat v1.3.13/go.mod h1:aE3+z4XlTJ+RzlZxdFiAIIJc1ikYLALAWtX9LqjQ87Q=
+github.com/gonvenience/term v1.0.2 h1:qKa2RydbWIrabGjR/fegJwpW5m+JvUwFL8mLhHzDXn0=
+github.com/gonvenience/term v1.0.2/go.mod h1:wThTR+3MzWtWn7XGVW6qQ65uaVf8GHED98KmwpuEQeo=
+github.com/gonvenience/text v1.0.7 h1:YmIqmgTwxnACYCG59DykgMbomwteYyNhAmEUEJtPl14=
+github.com/gonvenience/text v1.0.7/go.mod h1:OAjH+mohRszffLY6OjgQcUXiSkbrIavooFpfIt1ZwAs=
+github.com/gonvenience/wrap v1.2.0 h1:CwAoa60QIBVmQn/aUregAbk9FstEr17k9vCYpKF972c=
+github.com/gonvenience/wrap v1.2.0/go.mod h1:iNijaTmFD8+ORmNp9iS+dSBcCJrmIwwyoYLUngToGdk=
+github.com/gonvenience/ytbx v1.4.4 h1:jQopwyaLsVGuwdxSiN4WkXjsEaFNPJ3V4lUj7eyEpzo=
+github.com/gonvenience/ytbx v1.4.4/go.mod h1:w37+MKCPcCMY/jpPNmEklD4xKqrOAVBO6kIWW2+uI6M=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
@@ -262,12 +278,16 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/homeport/dyff v1.9.0 h1:2fvtDNvA3uEH8xurhfa9tJoKGtZ7UR7Z4mzmYdkdolg=
+github.com/homeport/dyff v1.9.0/go.mod h1:glKIR7tqPXcpciXc4vs0enwDaTP0LK8gbWrxCQyl95Q=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/ironcore-dev/controller-utils v0.9.4 h1:l+lXzDyTfDEvAn0o9KOX0JdeTf6uI0VKN9yVnl9jLM4=
+github.com/ironcore-dev/controller-utils v0.9.4/go.mod h1:0MS4W51EAEQo/nfajSaCj4RClju4MXv6IFGb+nDv2AA=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
@@ -287,6 +307,7 @@ github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -303,6 +324,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.8 h1:Ver94o/KW27O7MbhemLysbQUa6lCdvy5Ol62vcYn4Q0=
github.com/magiconair/properties v1.8.8/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -313,6 +336,8 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY
github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk=
+github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -324,12 +349,18 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=
+github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -360,6 +391,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
@@ -416,8 +451,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -447,6 +482,7 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -457,10 +493,14 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
+github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
+github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -641,6 +681,7 @@ google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/g
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
@@ -651,6 +692,8 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -704,10 +747,10 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMm
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kind v0.25.0 h1:ugUvgesHKKA0yKmD6QtYTiEev+kPUpGxdTPbMGf8VTU=
sigs.k8s.io/kind v0.25.0/go.mod h1:t7ueEpzPYJvHA8aeLtI52rtFftNgUYUaCwvxjk7phfw=
-sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0=
-sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY=
-sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U=
-sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag=
+sigs.k8s.io/kustomize/api v0.17.3 h1:6GCuHSsxq7fN5yhF2XrC+AAr8gxQwhexgHflOAD/JJU=
+sigs.k8s.io/kustomize/api v0.17.3/go.mod h1:TuDH4mdx7jTfK61SQ/j1QZM/QWR+5rmEiNjvYlhzFhc=
+sigs.k8s.io/kustomize/kyaml v0.17.2 h1:+AzvoJUY0kq4QAhH/ydPHHMRLijtUKiyVyh7fOSshr0=
+sigs.k8s.io/kustomize/kyaml v0.17.2/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
diff --git a/internal/data/kustomization.yaml b/internal/data/kustomization.yaml
new file mode 100644
index 00000000..bb092c64
--- /dev/null
+++ b/internal/data/kustomization.yaml
@@ -0,0 +1,9 @@
+# Labels to add to all resources.
+labels:
+ - pairs:
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/instance: $RELEASE_NAME
+ includeSelectors: false
+
+resources:
+ - rendered-manifests.yaml
diff --git a/internal/helm_client.go b/internal/helm_client.go
index 0e95f4b1..dddedee7 100644
--- a/internal/helm_client.go
+++ b/internal/helm_client.go
@@ -17,12 +17,18 @@ limitations under the License.
package internal
import (
+ "bytes"
"context"
+ _ "embed"
"fmt"
+ "io"
"net/url"
"os"
"path"
+ "strings"
+ "github.com/databus23/helm-diff/v3/diff"
+ "github.com/databus23/helm-diff/v3/manifest"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
@@ -32,6 +38,7 @@ import (
helmCli "helm.sh/helm/v3/pkg/cli"
helmVals "helm.sh/helm/v3/pkg/cli/values"
helmGetter "helm.sh/helm/v3/pkg/getter"
+ "helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/registry"
helmRelease "helm.sh/helm/v3/pkg/release"
helmDriver "helm.sh/helm/v3/pkg/storage/driver"
@@ -41,6 +48,8 @@ import (
"k8s.io/utils/ptr"
addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/kustomize/api/krusty"
+ "sigs.k8s.io/kustomize/kyaml/filesys"
)
type Client interface {
@@ -49,8 +58,27 @@ type Client interface {
UninstallHelmRelease(ctx context.Context, restConfig *rest.Config, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.UninstallReleaseResponse, error)
}
+var (
+ _ Client = (*HelmClient)(nil)
+ _ postrender.PostRenderer = (*releaseDriftPostRenderer)(nil)
+)
+
+//go:embed data/kustomization.yaml
+var releaseDriftKustomization string
+
type HelmClient struct{}
+type HelmInstallOverride struct {
+ DryRun bool
+ DisableHooks bool
+ SkipCRDs bool
+ IsUpgrade bool
+}
+
+type releaseDriftPostRenderer struct {
+ releaseName string
+}
+
// GetActionConfig returns a new Helm action configuration.
func GetActionConfig(ctx context.Context, namespace string, config *rest.Config) (*helmAction.Configuration, error) {
log := ctrl.LoggerFrom(ctx)
@@ -103,7 +131,7 @@ func (c *HelmClient) InstallOrUpgradeHelmRelease(ctx context.Context, restConfig
existingRelease, err := c.GetHelmRelease(ctx, restConfig, spec)
if err != nil {
if errors.Is(err, helmDriver.ErrReleaseNotFound) {
- return c.InstallHelmRelease(ctx, restConfig, credentialsPath, caFilePath, spec)
+ return c.InstallHelmRelease(ctx, restConfig, credentialsPath, caFilePath, spec, nil)
}
return nil, err
@@ -113,7 +141,7 @@ func (c *HelmClient) InstallOrUpgradeHelmRelease(ctx context.Context, restConfig
}
// generateHelmInstallConfig generates default helm install config using helmOptions specified in HCP CR spec.
-func generateHelmInstallConfig(actionConfig *helmAction.Configuration, helmOptions *addonsv1alpha1.HelmOptions) *helmAction.Install {
+func generateHelmInstallConfig(actionConfig *helmAction.Configuration, helmOptions *addonsv1alpha1.HelmOptions, overrideOptions *HelmInstallOverride) *helmAction.Install {
installClient := helmAction.NewInstall(actionConfig)
installClient.CreateNamespace = true
if actionConfig.RegistryClient != nil {
@@ -135,6 +163,12 @@ func generateHelmInstallConfig(actionConfig *helmAction.Configuration, helmOptio
installClient.Atomic = helmOptions.Atomic
installClient.IncludeCRDs = helmOptions.Install.IncludeCRDs
installClient.CreateNamespace = helmOptions.Install.CreateNamespace
+ if overrideOptions != nil {
+ installClient.SkipCRDs = overrideOptions.SkipCRDs
+ installClient.DisableHooks = overrideOptions.DisableHooks
+ installClient.DryRun = overrideOptions.DryRun
+ installClient.IsUpgrade = overrideOptions.IsUpgrade
+ }
return installClient
}
@@ -170,7 +204,7 @@ func generateHelmUpgradeConfig(actionConfig *helmAction.Configuration, helmOptio
}
// InstallHelmRelease installs a Helm release.
-func (c *HelmClient) InstallHelmRelease(ctx context.Context, restConfig *rest.Config, credentialsPath, caFilePath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) {
+func (c *HelmClient) InstallHelmRelease(ctx context.Context, restConfig *rest.Config, credentialsPath, caFilePath string, spec addonsv1alpha1.HelmReleaseProxySpec, overrideOptions *HelmInstallOverride) (*helmRelease.Release, error) {
log := ctrl.LoggerFrom(ctx)
settings, actionConfig, err := HelmInit(ctx, spec.ReleaseNamespace, restConfig)
@@ -194,10 +228,13 @@ func (c *HelmClient) InstallHelmRelease(ctx context.Context, restConfig *rest.Co
return nil, err
}
- installClient := generateHelmInstallConfig(actionConfig, &spec.Options)
+ installClient := generateHelmInstallConfig(actionConfig, &spec.Options, overrideOptions)
installClient.RepoURL = repoURL
installClient.Version = spec.Version
installClient.Namespace = spec.ReleaseNamespace
+ if spec.ReleaseDrift {
+ installClient.PostRenderer = releaseDriftPostRenderer{releaseName: spec.ReleaseName}
+ }
if spec.ReleaseName == "" {
installClient.GenerateName = true
@@ -253,6 +290,18 @@ func (c *HelmClient) InstallHelmRelease(ctx context.Context, restConfig *rest.Co
return installClient.RunWithContext(ctx, chartRequested, vals) // Can return error and a release
}
+// TemplateHelmRelease generate a template for the Helm release.
+func (c *HelmClient) TemplateHelmRelease(ctx context.Context, restConfig *rest.Config, credentialsPath, caFilePath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) {
+ overrideOptions := &HelmInstallOverride{
+ DryRun: true,
+ DisableHooks: true,
+ SkipCRDs: true,
+ IsUpgrade: true,
+ }
+
+ return c.InstallHelmRelease(ctx, restConfig, credentialsPath, caFilePath, spec, overrideOptions)
+}
+
// newDefaultRegistryClient creates registry client object with default config which can be used to install/upgrade helm charts.
func newDefaultRegistryClient(credentialsPath string, enableCache bool, caFilePath string, insecureSkipTLSVerify bool) (*registry.Client, error) {
if caFilePath == "" && !insecureSkipTLSVerify {
@@ -318,6 +367,9 @@ func (c *HelmClient) UpgradeHelmReleaseIfChanged(ctx context.Context, restConfig
upgradeClient.RepoURL = repoURL
upgradeClient.Version = spec.Version
upgradeClient.Namespace = spec.ReleaseNamespace
+ if spec.ReleaseDrift {
+ upgradeClient.PostRenderer = releaseDriftPostRenderer{releaseName: spec.ReleaseName}
+ }
log.V(2).Info("Locating chart...")
cp, err := upgradeClient.ChartPathOptions.LocateChart(chartName, settings)
@@ -363,7 +415,7 @@ func (c *HelmClient) UpgradeHelmReleaseIfChanged(ctx context.Context, restConfig
return nil, errors.Errorf("failed to load request chart %s", chartName)
}
- shouldUpgrade, err := shouldUpgradeHelmRelease(ctx, *existing, chartRequested, vals)
+ shouldUpgrade, err := c.shouldUpgradeHelmRelease(ctx, restConfig, credentialsPath, caFilePath, *existing, chartRequested, vals, spec)
if err != nil {
return nil, err
}
@@ -396,7 +448,7 @@ func writeValuesToFile(ctx context.Context, spec addonsv1alpha1.HelmReleaseProxy
}
// shouldUpgradeHelmRelease determines if a Helm release should be upgraded.
-func shouldUpgradeHelmRelease(ctx context.Context, existing helmRelease.Release, chartRequested *chart.Chart, values map[string]interface{}) (bool, error) {
+func (c *HelmClient) shouldUpgradeHelmRelease(ctx context.Context, restConfig *rest.Config, credentialsPath, caFilePath string, existing helmRelease.Release, chartRequested *chart.Chart, values map[string]interface{}, spec addonsv1alpha1.HelmReleaseProxySpec) (bool, error) {
log := ctrl.LoggerFrom(ctx)
if existing.Chart == nil || existing.Chart.Metadata == nil {
@@ -425,7 +477,31 @@ func shouldUpgradeHelmRelease(ctx context.Context, existing helmRelease.Release,
return false, errors.Wrapf(err, "failed to new release values")
}
- return !cmp.Equal(oldValues, newValues), nil
+ if !cmp.Equal(oldValues, newValues) {
+ return true, nil
+ }
+
+ if spec.ReleaseDrift {
+ klog.V(2).Info("release drift is enabled. Trying to detect release diff")
+ install, err := c.TemplateHelmRelease(ctx, restConfig, credentialsPath, caFilePath, spec)
+ if err != nil {
+ return false, errors.Wrapf(err, "failed to generate release template")
+ }
+ _, actionConfig, err := HelmInit(ctx, spec.ReleaseNamespace, restConfig)
+ if err != nil {
+ return false, errors.Wrapf(err, "failed to init helm client")
+ }
+ releaseManifest, installManifest, err := manifest.Generate(actionConfig, []byte(existing.Manifest), []byte(install.Manifest))
+ if err != nil {
+ return false, errors.Wrapf(err, "failed to generate existing and installing release manifests")
+ }
+ currentSpecs := manifest.Parse(string(releaseManifest), spec.ReleaseNamespace, true, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook)
+ newSpecs := manifest.Parse(string(installManifest), spec.ReleaseNamespace, true, manifest.Helm3TestHook, manifest.Helm2TestSuccessHook)
+
+ return diff.Manifests(currentSpecs, newSpecs, &diff.Options{}, io.Discard), nil
+ }
+
+ return false, nil
}
// GetHelmRelease returns a Helm release if it exists.
@@ -511,3 +587,25 @@ func (c *HelmClient) RollbackHelmRelease(ctx context.Context, restConfig *rest.C
return rollbackClient.Run(spec.ReleaseName)
}
+
+func (r releaseDriftPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
+ fSys := filesys.MakeFsInMemory()
+ if err := fSys.WriteFile("kustomization.yaml", []byte(strings.Replace(releaseDriftKustomization, "$RELEASE_NAME", r.releaseName, 1))); err != nil {
+ return nil, err
+ }
+ if err := fSys.WriteFile("rendered-manifests.yaml", renderedManifests.Bytes()); err != nil {
+ return nil, err
+ }
+
+ kustomizer := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
+ m, err := kustomizer.Run(fSys, ".")
+ if err != nil {
+ return nil, err
+ }
+ yml, err := m.AsYaml()
+ if err != nil {
+ return nil, err
+ }
+
+ return bytes.NewBuffer(yml), nil
+}
diff --git a/test/e2e/capi_test.go b/test/e2e/capi_test.go
index 5e3df29e..f5f85568 100644
--- a/test/e2e/capi_test.go
+++ b/test/e2e/capi_test.go
@@ -25,7 +25,7 @@ import (
. "github.com/onsi/ginkgo/v2"
"k8s.io/utils/ptr"
- capi_e2e "sigs.k8s.io/cluster-api/test/e2e"
+ capie2e "sigs.k8s.io/cluster-api/test/e2e"
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
)
@@ -36,8 +36,8 @@ var _ = Describe("Running the Cluster API E2E tests", func() {
})
Context("Running the quick-start spec [PR-Blocking]", func() {
- capi_e2e.QuickStartSpec(context.TODO(), func() capi_e2e.QuickStartSpecInput {
- return capi_e2e.QuickStartSpecInput{
+ capie2e.QuickStartSpec(context.TODO(), func() capie2e.QuickStartSpecInput {
+ return capie2e.QuickStartSpecInput{
E2EConfig: e2eConfig,
ClusterctlConfigPath: clusterctlConfigPath,
BootstrapClusterProxy: bootstrapClusterProxy,
@@ -52,8 +52,8 @@ var _ = Describe("Running the Cluster API E2E tests", func() {
})
Context("Running the workload cluster K8s version upgrade spec [K8s-Upgrade]", func() {
- capi_e2e.ClusterUpgradeConformanceSpec(context.TODO(), func() capi_e2e.ClusterUpgradeConformanceSpecInput {
- return capi_e2e.ClusterUpgradeConformanceSpecInput{
+ capie2e.ClusterUpgradeConformanceSpec(context.TODO(), func() capie2e.ClusterUpgradeConformanceSpecInput {
+ return capie2e.ClusterUpgradeConformanceSpecInput{
E2EConfig: e2eConfig,
ClusterctlConfigPath: clusterctlConfigPath,
BootstrapClusterProxy: bootstrapClusterProxy,
@@ -71,8 +71,8 @@ var _ = Describe("Running the Cluster API E2E tests", func() {
Context("upgrade from an old version of v1beta1 to current, and scale workload clusters created in the old version", func() {
- capi_e2e.ClusterctlUpgradeSpec(context.TODO(), func() capi_e2e.ClusterctlUpgradeSpecInput {
- return capi_e2e.ClusterctlUpgradeSpecInput{
+ capie2e.ClusterctlUpgradeSpec(context.TODO(), func() capie2e.ClusterctlUpgradeSpecInput {
+ return capie2e.ClusterctlUpgradeSpecInput{
E2EConfig: e2eConfig,
ClusterctlConfigPath: clusterctlConfigPath,
BootstrapClusterProxy: bootstrapClusterProxy,
diff --git a/test/e2e/common.go b/test/e2e/common.go
index 4076f60c..f035ddb1 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -44,10 +44,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)
-const (
- kubesystem = "kube-system"
-)
-
// EnsureControlPlaneInitialized waits for the cluster KubeadmControlPlane object to be initialized
// and then installs cloud-provider-azure components via Helm.
// Fulfills the clusterctl.Waiter type so that it can be used as ApplyClusterTemplateAndWaitInput data
@@ -75,7 +71,7 @@ func EnsureControlPlaneInitialized(ctx context.Context, input clusterctl.ApplyCu
Eventually(func(g Gomega) {
ns := &corev1.Namespace{}
clusterProxy := input.ClusterProxy.GetWorkloadCluster(ctx, input.Namespace, input.ClusterName)
- g.Expect(clusterProxy.GetClient().Get(ctx, client.ObjectKey{Name: kubesystem}, ns)).To(Succeed(), "Failed to get kube-system namespace")
+ g.Expect(clusterProxy.GetClient().Get(ctx, client.ObjectKey{Name: metav1.NamespaceSystem}, ns)).To(Succeed(), "Failed to get kube-system namespace")
}, input.WaitForControlPlaneIntervals...).Should(Succeed(), "API Server was not reachable in time")
By("Ensure calico is ready after control plane is initialized")
@@ -96,7 +92,7 @@ const (
calicoHelmChartName string = "tigera-operator"
)
-// EnsureCalicoIsReady verifies that the calico deployments exist and and are available on the workload cluster.
+// EnsureCalicoIsReady verifies that the calico deployments exist and are available on the workload cluster.
func EnsureCalicoIsReady(ctx context.Context, input clusterctl.ApplyCustomClusterTemplateAndWaitInput) {
specName := "ensure-calico"
@@ -275,7 +271,7 @@ func createApplyClusterTemplateInput(specName string, changes ...func(*clusterct
KubeconfigPath: bootstrapClusterProxy.GetKubeconfigPath(),
InfrastructureProvider: clusterctl.DefaultInfrastructureProvider,
Flavor: clusterctl.DefaultFlavor,
- Namespace: "default",
+ Namespace: metav1.NamespaceDefault,
ClusterName: "cluster",
KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion),
ControlPlaneMachineCount: ptr.To[int64](1),
diff --git a/test/e2e/config/helm.yaml b/test/e2e/config/helm.yaml
index 18fad1d3..7fdbb7c0 100644
--- a/test/e2e/config/helm.yaml
+++ b/test/e2e/config/helm.yaml
@@ -192,6 +192,7 @@ intervals:
default/wait-helmreleaseproxy-ready: ["10m", "10s"]
default/wait-helm-release: ["10m", "10s"]
default/wait-helm-release-deployed: ["10m", "10s"]
+ default/wait-helm-release-drift: [ "1m", "10s" ]
default/wait-delete-helmreleaseproxy: ["3m", "10s"]
node-drain/wait-deployment-available: ["3m", "10s"]
node-drain/wait-control-plane: ["15m", "10s"]
diff --git a/test/e2e/helm_releasedrift.go b/test/e2e/helm_releasedrift.go
new file mode 100644
index 00000000..43015a97
--- /dev/null
+++ b/test/e2e/helm_releasedrift.go
@@ -0,0 +1,119 @@
+//go:build e2e
+// +build e2e
+
+/*
+Copyright 2024 The Kubernetes 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 e2e
+
+import (
+ "context"
+ "fmt"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ appsv1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/utils/ptr"
+ addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
+ "sigs.k8s.io/cluster-api-addon-provider-helm/controllers/helmreleasedrift"
+ ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "sigs.k8s.io/cluster-api/test/framework"
+)
+
+type ValidationType string
+
+const (
+ ValidationEventually ValidationType = "Eventually"
+ ValidationConsistently ValidationType = "Consistently"
+)
+
+// HelmReleaseDriftInput specifies the input for Helm release drift Deployment validation and verifying that it was successful.
+type HelmReleaseDriftInput struct {
+ BootstrapClusterProxy framework.ClusterProxy
+ Namespace *corev1.Namespace
+ ClusterName string
+ HelmChartProxy *addonsv1alpha1.HelmChartProxy
+ UpdatedDeploymentReplicas int32
+ ExpectedDeploymentReplicas int32
+ ExpectedRevision int
+ Validation ValidationType
+}
+
+func HelmReleaseDriftWithDeployment(ctx context.Context, inputGetter func() HelmReleaseDriftInput) {
+ var (
+ specName = "helm-upgrade"
+ input HelmReleaseDriftInput
+ mgmtClient ctrlclient.Client
+ )
+ input = inputGetter()
+ hcp := input.HelmChartProxy
+ Expect(input.Validation).NotTo(BeEmpty(), "HelmReleaseDriftInput must contains validation type to be defined")
+
+ // Get workload Cluster proxy
+ By("creating a clusterctl proxy to the workload cluster")
+ workloadClusterProxy := bootstrapClusterProxy.GetWorkloadCluster(ctx, input.Namespace.Name, input.ClusterName)
+ Expect(workloadClusterProxy).NotTo(BeNil())
+ mgmtClient = workloadClusterProxy.GetClient()
+
+ deploymentList := &appsv1.DeploymentList{}
+ err := mgmtClient.List(ctx, deploymentList, ctrlclient.InNamespace(hcp.Spec.ReleaseNamespace), ctrlclient.MatchingLabels{helmreleasedrift.InstanceLabelKey: hcp.Spec.ReleaseName})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(deploymentList.Items).NotTo(BeEmpty())
+
+ deployment := &deploymentList.Items[0]
+ patch := ctrlclient.MergeFrom(deployment.DeepCopy())
+ deployment.Spec.Replicas = ptr.To(input.UpdatedDeploymentReplicas)
+ err = mgmtClient.Patch(ctx, deployment, patch)
+ Expect(err).NotTo(HaveOccurred())
+
+ deploymentName := ctrlclient.ObjectKeyFromObject(deployment)
+ if input.Validation == ValidationEventually {
+ // Wait for Helm release Deployment replicas to be returned back
+ Eventually(func() error {
+ if err = mgmtClient.Get(ctx, deploymentName, deployment); err != nil {
+ return err
+ }
+ if *deployment.Spec.Replicas != input.ExpectedDeploymentReplicas && deployment.Status.ReadyReplicas != input.ExpectedDeploymentReplicas {
+ return fmt.Errorf("expected Deployment replicas to be %d, got %d", input.ExpectedDeploymentReplicas, deployment.Status.ReadyReplicas)
+ }
+
+ return nil
+ }, e2eConfig.GetIntervals("default", "wait-helm-release-drift")...).Should(Succeed())
+ }
+ if input.Validation == ValidationConsistently {
+ Consistently(func() error {
+ if err = mgmtClient.Get(ctx, deploymentName, deployment); err != nil {
+ return err
+ }
+ if *deployment.Spec.Replicas != input.ExpectedDeploymentReplicas && deployment.Status.ReadyReplicas != input.ExpectedDeploymentReplicas {
+ return fmt.Errorf("expected Deployment replicas to be %d, got %d", input.ExpectedDeploymentReplicas, deployment.Status.ReadyReplicas)
+ }
+
+ return nil
+ }, e2eConfig.GetIntervals("default", "wait-helm-release-drift")...).Should(Succeed())
+ }
+
+ helmUpgradeInput := HelmUpgradeInput{
+ BootstrapClusterProxy: workloadClusterProxy,
+ Namespace: input.Namespace,
+ ClusterName: input.ClusterName,
+ HelmChartProxy: input.HelmChartProxy,
+ ExpectedRevision: input.ExpectedRevision,
+ }
+ EnsureHelmReleaseInstallOrUpgrade(ctx, specName, input.BootstrapClusterProxy, nil, &helmUpgradeInput, true)
+}
diff --git a/test/e2e/helm_test.go b/test/e2e/helm_test.go
index 4dc460ad..7364d51f 100644
--- a/test/e2e/helm_test.go
+++ b/test/e2e/helm_test.go
@@ -33,7 +33,7 @@ import (
"k8s.io/apimachinery/pkg/types"
addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
- capi_e2e "sigs.k8s.io/cluster-api/test/e2e"
+ capie2e "sigs.k8s.io/cluster-api/test/e2e"
"sigs.k8s.io/cluster-api/test/framework"
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
"sigs.k8s.io/cluster-api/util"
@@ -72,7 +72,7 @@ var _ = Describe("Workload cluster creation", func() {
Expect(clusterctlConfigPath).To(BeAnExistingFile(), "Invalid argument. clusterctlConfigPath must be an existing file when calling %s spec", specName)
Expect(bootstrapClusterProxy).NotTo(BeNil(), "Invalid argument. bootstrapClusterProxy can't be nil when calling %s spec", specName)
Expect(os.MkdirAll(artifactFolder, 0o755)).To(Succeed(), "Invalid argument. artifactFolder can't be created for %s spec", specName)
- Expect(e2eConfig.Variables).To(HaveKey(capi_e2e.KubernetesVersion))
+ Expect(e2eConfig.Variables).To(HaveKey(capie2e.KubernetesVersion))
// CLUSTER_NAME and CLUSTER_NAMESPACE allows for testing existing clusters.
// If CLUSTER_NAMESPACE is set, don't generate a new prefix. Otherwise,
@@ -219,6 +219,102 @@ var _ = Describe("Workload cluster creation", func() {
})
})
})
+
+ It("Install and manage Helm chart with ReleaseDrift option enabled", func() {
+ clusterName = fmt.Sprintf("%s-%s", specName, util.RandomString(6))
+ clusterctl.ApplyClusterTemplateAndWait(ctx, createApplyClusterTemplateInput(
+ specName,
+ withNamespace(namespace.Name),
+ withClusterName(clusterName),
+ withControlPlaneMachineCount(1),
+ withWorkerMachineCount(1),
+ withControlPlaneWaiters(clusterctl.ControlPlaneWaiters{
+ WaitForControlPlaneInitialized: EnsureControlPlaneInitialized,
+ }),
+ ), result)
+
+ hcp := &addonsv1alpha1.HelmChartProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ahoy",
+ Namespace: namespace.Name,
+ },
+ Spec: addonsv1alpha1.HelmChartProxySpec{
+ ClusterSelector: metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "nginxIngress": "enabled",
+ },
+ },
+ ChartName: "hello-world",
+ RepoURL: "https://helm.github.io/examples",
+ ReleaseName: "ahoy",
+ ReleaseNamespace: "ahoy-namespace",
+ ReconcileStrategy: string(addonsv1alpha1.ReconcileStrategyContinuous),
+ ReleaseDrift: true,
+ Options: addonsv1alpha1.HelmOptions{
+ Wait: true,
+ Timeout: &metav1.Duration{Duration: 5 * time.Minute},
+ },
+ },
+ }
+
+ // Create new Helm chart
+ By("Creating new HelmChartProxy to install hello-world chart", func() {
+ HelmInstallSpec(ctx, func() HelmInstallInput {
+ return HelmInstallInput{
+ BootstrapClusterProxy: bootstrapClusterProxy,
+ Namespace: namespace,
+ ClusterName: clusterName,
+ HelmChartProxy: hcp,
+ }
+ })
+ })
+
+ // Updating hello-world deployment and waiting for the release drift
+ By("Updating hello-world deployment and waiting for release drift", func() {
+ HelmReleaseDriftWithDeployment(ctx, func() HelmReleaseDriftInput {
+ return HelmReleaseDriftInput{
+ BootstrapClusterProxy: bootstrapClusterProxy,
+ Namespace: namespace,
+ ClusterName: clusterName,
+ HelmChartProxy: hcp,
+ UpdatedDeploymentReplicas: 2,
+ ExpectedDeploymentReplicas: 1,
+ ExpectedRevision: 2,
+ Validation: ValidationEventually,
+ }
+ })
+ })
+
+ // Update existing Helm chart
+ By("Updating HelmChartProxy disabling release drift option", func() {
+ hcp.Spec.ReleaseDrift = false
+ HelmUpgradeSpec(ctx, func() HelmUpgradeInput {
+ return HelmUpgradeInput{
+ BootstrapClusterProxy: bootstrapClusterProxy,
+ Namespace: namespace,
+ ClusterName: clusterName,
+ HelmChartProxy: hcp,
+ ExpectedRevision: 1,
+ }
+ })
+ })
+
+ // Updating hello-world deployment and waiting for the release drift
+ By("Updating hello-world deployment and waiting release drift to be inactive for a long time", func() {
+ HelmReleaseDriftWithDeployment(ctx, func() HelmReleaseDriftInput {
+ return HelmReleaseDriftInput{
+ BootstrapClusterProxy: bootstrapClusterProxy,
+ Namespace: namespace,
+ ClusterName: clusterName,
+ HelmChartProxy: hcp,
+ UpdatedDeploymentReplicas: 2,
+ ExpectedDeploymentReplicas: 2,
+ ExpectedRevision: 1,
+ Validation: ValidationConsistently,
+ }
+ })
+ })
+ })
})
Context("Creating workload cluster [REQUIRED]", func() {
diff --git a/test/e2e/helpers.go b/test/e2e/helpers.go
index fd896071..b438c23c 100644
--- a/test/e2e/helpers.go
+++ b/test/e2e/helpers.go
@@ -74,10 +74,13 @@ type deploymentsClientAdapter struct {
// Get fetches the deployment named by the key and updates the provided object.
func (c deploymentsClientAdapter) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
deployment, err := c.client.Get(ctx, key.Name, metav1.GetOptions{})
+ if err != nil {
+ return err
+ }
if deployObj, ok := obj.(*appsv1.Deployment); ok {
deployment.DeepCopyInto(deployObj)
}
- return err
+ return nil
}
// WaitForDeploymentsAvailableInput is the input for WaitForDeploymentsAvailable.
@@ -172,7 +175,6 @@ func prettyPrint(v interface{}) string {
}
func getHelmActionConfigForTests(_ context.Context, workloadClusterProxy framework.ClusterProxy, releaseNamespace string) *helmAction.Configuration {
-
workloadKubeconfigPath := workloadClusterProxy.GetKubeconfigPath()
settings := helmCli.New()
@@ -228,7 +230,7 @@ type WaitForHelmReleaseDeployedInput struct {
}
// WaitForHelmReleaseDeployed waits until the Helm release has status.Status = deployed, which signals that the Helm release was successfully deployed.
-func WaitForHelmReleaseDeployed(ctx context.Context, input WaitForHelmReleaseDeployedInput, intervals ...interface{}) *helmRelease.Release {
+func WaitForHelmReleaseDeployed(_ context.Context, input WaitForHelmReleaseDeployedInput, intervals ...interface{}) *helmRelease.Release {
start := time.Now()
Expect(input.HelmRelease).ToNot(BeNil())
getClient := helmAction.NewGet(input.ActionConfig)
@@ -246,7 +248,7 @@ func WaitForHelmReleaseDeployed(ctx context.Context, input WaitForHelmReleaseDep
return false
}, intervals...).Should(BeTrue(), fmt.Sprintf("HelmRelease %s/%s failed to deploy, status: %s", input.Namespace, input.HelmRelease.Name, input.HelmRelease.Info.Status))
- Logf("Helm release %s is now deployed, took %v", input.HelmRelease, time.Since(start))
+ Logf("Helm release %s/%s is now deployed, took %v", input.Namespace, input.HelmRelease.Name, time.Since(start))
return input.HelmRelease
}
@@ -310,6 +312,8 @@ func GetWaitForHelmReleaseProxyReadyInput(ctx context.Context, clusterProxy fram
return errors.Errorf("Non-generated ReleaseName mismatch, got `%s` but HelmChartProxy specifies `%s`", hrp.Spec.ReleaseName, helmChartProxy.Spec.ReleaseName)
case hrp.Spec.ReleaseNamespace != helmChartProxy.Spec.ReleaseNamespace:
return errors.Errorf("ReleaseNamespace mismatch, got `%s` but HelmChartProxy specifies `%s`", hrp.Spec.ReleaseNamespace, helmChartProxy.Spec.ReleaseNamespace)
+ case hrp.Spec.ReleaseDrift != helmChartProxy.Spec.ReleaseDrift:
+ return errors.Errorf("ReleaseDrift mismatch, got `%t` but HelmChartProxy specifies `%t`", hrp.Spec.ReleaseDrift, helmChartProxy.Spec.ReleaseDrift)
}
// If we made it past all the checks, then we have the correct HelmReleaseProxy.
@@ -355,11 +359,15 @@ func normalizeHelmReleaseValues(_ context.Context, helmReleaseProxy *addonsv1alp
// Normalize the HelmReleaseProxy values.
var normalizedValues map[string]interface{}
- Expect(yaml.Unmarshal([]byte(helmReleaseProxy.Spec.Values), &normalizedValues)).To(Succeed())
+ if helmReleaseProxy.Spec.Values != "" {
+ Expect(yaml.Unmarshal([]byte(helmReleaseProxy.Spec.Values), &normalizedValues)).To(Succeed())
+ }
// Normalize the Helm release values.
var normalizedReleaseValues map[string]interface{}
- Expect(yaml.Unmarshal(releaseValues, &normalizedReleaseValues)).To(Succeed())
+ if helmReleaseProxy.Spec.Values != "" {
+ Expect(yaml.Unmarshal(releaseValues, &normalizedReleaseValues)).To(Succeed())
+ }
// Normalize the Helm release values.
Expect(normalizedReleaseValues).To(Equal(normalizedValues))