Skip to content

✨ [WIP] [POC] Helm release drift manager based on dynamic manifest objects watch #334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<a href="https://cluster-api.sigs.k8s.io"><img alt="capi" src="./logos/kubernetes-cluster-logos_final-02.svg" width="160x" /></a>
<p>
<a href="https://godoc.org/sigs.k8s.io/cluster-api"><img src="https://godoc.org/sigs.k8s.io/cluster-api?status.svg"></a>
<!-- join kubernetes slack channel for cluster-api -->
<!-- join kubernetes Slack channel for cluster-api -->
<a href="http://slack.k8s.io/">
<img src="https://img.shields.io/badge/join%20slack-%23cluster--api-brightgreen"></a>
</p>
Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha1/helmchartproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions api/v1alpha1/helmreleaseproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
146 changes: 146 additions & 0 deletions controllers/helmreleasedrift/manager.go
Original file line number Diff line number Diff line change
@@ -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
}
93 changes: 93 additions & 0 deletions controllers/helmreleasedrift/manager_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading
Loading