diff --git a/api/v1alpha1/helmchartproxy_types.go b/api/v1alpha1/helmchartproxy_types.go index 6ecfda6..0318f89 100644 --- a/api/v1alpha1/helmchartproxy_types.go +++ b/api/v1alpha1/helmchartproxy_types.go @@ -84,6 +84,8 @@ type HelmChartProxySpec struct { // +optional ReconcileStrategy string `json:"reconcileStrategy,omitempty"` + 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 1ec60c9..76ffa30 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 7b7b30b..b8c987b 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 08094ac..905a653 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 ac8edaf..e8ebde2 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 diff --git a/controllers/helmreleasedrift/manager.go b/controllers/helmreleasedrift/manager.go new file mode 100644 index 0000000..3ba4199 --- /dev/null +++ b/controllers/helmreleasedrift/manager.go @@ -0,0 +1,145 @@ +/* +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" +) + +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: "Helm", + 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 0000000..904f818 --- /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 0000000..1c4a910 --- /dev/null +++ b/controllers/helmreleasedrift/releasedrift_controller.go @@ -0,0 +1,80 @@ +/* +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" + + 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 + }, +} + +// 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 0000000..1be3403 --- /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/fakemanifest_controller.go b/controllers/helmreleasedrift/test/fake/fakemanifest_controller.go new file mode 100644 index 0000000..7e9a4b9 --- /dev/null +++ b/controllers/helmreleasedrift/test/fake/fakemanifest_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 0000000..8409560 --- /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 07f66ad..a1615b9 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)) } diff --git a/go.mod b/go.mod index 5da8e8d..a6a093d 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,19 @@ 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/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 +26,7 @@ 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 ) require ( @@ -172,7 +175,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,8 +202,7 @@ 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/kustomize/kyaml v0.17.2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 763b810..f5cf19c 100644 --- a/go.sum +++ b/go.sum @@ -268,6 +268,8 @@ 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= @@ -416,8 +418,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.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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= @@ -704,10 +706,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/helm_client.go b/internal/helm_client.go index 0e95f4b..1857202 100644 --- a/internal/helm_client.go +++ b/internal/helm_client.go @@ -49,6 +49,8 @@ type Client interface { UninstallHelmRelease(ctx context.Context, restConfig *rest.Config, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.UninstallReleaseResponse, error) } +var _ Client = (*HelmClient)(nil) + type HelmClient struct{} // GetActionConfig returns a new Helm action configuration.