diff --git a/README.md b/README.md index 269412d6..6339ee3d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ capi

- +

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))