Skip to content
Merged
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
4 changes: 4 additions & 0 deletions api/v1beta1/clustersummary_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ type HelmChartSummary struct {
// +optional
ValuesHash []byte `json:"valuesHash,omitempty"`

// PatchesHash represents of a unique value for the patches section
// +optional
PatchesHash []byte `json:"patchesHash,omitempty"`

// Status indicates whether ClusterSummary can manage the helm
// chart or there is a conflict
// +optional
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -1674,6 +1674,11 @@ spec:
description: FailureMessage provides the specific error from
the Helm engine for this release
type: string
patchesHash:
description: PatchesHash represents of a unique value for the
patches section
format: byte
type: string
releaseName:
description: ReleaseName is the chart release
minLength: 1
Expand Down
57 changes: 53 additions & 4 deletions controllers/handlers_helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1105,9 +1105,9 @@ func getFailureMessageFromHelmChartSummary(requestedChart *configv1beta1.HelmCha
return nil
}

// getValueHashFromHelmChartSummary returns the valueHash stored for this chart
// getValuesHashFromHelmChartSummary returns the valueHash stored for this chart
// in the ClusterSummary
func getValueHashFromHelmChartSummary(requestedChart *configv1beta1.HelmChart,
func getValuesHashFromHelmChartSummary(requestedChart *configv1beta1.HelmChart,
clusterSummary *configv1beta1.ClusterSummary) []byte {

for i := range clusterSummary.Status.HelmReleaseSummaries {
Expand All @@ -1122,6 +1122,23 @@ func getValueHashFromHelmChartSummary(requestedChart *configv1beta1.HelmChart,
return nil
}

// getPatchesHashFromHelmChartSummary returns the patchesHash stored for this chart
// in the ClusterSummary
func getPatchesHashFromHelmChartSummary(requestedChart *configv1beta1.HelmChart,
clusterSummary *configv1beta1.ClusterSummary) []byte {

for i := range clusterSummary.Status.HelmReleaseSummaries {
rs := &clusterSummary.Status.HelmReleaseSummaries[i]
if rs.ReleaseName == requestedChart.ReleaseName &&
rs.ReleaseNamespace == requestedChart.ReleaseNamespace {

return rs.PatchesHash
}
}

return nil
}

func generateConflictForHelmChart(ctx context.Context, clusterSummary *configv1beta1.ClusterSummary,
instantiatedChart *configv1beta1.HelmChart) string {

Expand Down Expand Up @@ -2297,10 +2314,18 @@ func shouldUpgrade(ctx context.Context, currentRelease *releaseInfo, instantiate
return false
}

currentPatchesHash, err := getHelmChartPatchesHash(ctx, clusterSummary, logger)
if err != nil {
logger.V(logs.LogInfo).Info(fmt.Sprintf("failed to get current patches hash: %v", err))
currentPatchesHash = []byte("")
}

if clusterSummary.Spec.ClusterProfileSpec.SyncMode != configv1beta1.SyncModeContinuousWithDriftDetection {
if clusterSummary.Spec.ClusterProfileSpec.SyncMode != configv1beta1.SyncModeDryRun {
oldValueHash := getValueHashFromHelmChartSummary(instantiatedChart, clusterSummary)
oldValueHash := getValuesHashFromHelmChartSummary(instantiatedChart, clusterSummary)
oldPatchesHash := getPatchesHashFromHelmChartSummary(instantiatedChart, clusterSummary)

// Compare Values
c := getManagementClusterClient()
currentValueHash, err := getHelmChartValuesHash(ctx, c, instantiatedChart, clusterSummary, mgmtResources, logger)
if err != nil {
Expand All @@ -2310,6 +2335,11 @@ func shouldUpgrade(ctx context.Context, currentRelease *releaseInfo, instantiate
if !reflect.DeepEqual(oldValueHash, currentValueHash) {
return true
}

// Compare patches
if !reflect.DeepEqual(oldPatchesHash, currentPatchesHash) {
return true
}
}

if currentRelease != nil {
Expand Down Expand Up @@ -2560,6 +2590,11 @@ func updateStatusForReferencedHelmReleases(ctx context.Context, c client.Client,

conflict := false

patchesHash, err := getPatchesHash(ctx, clusterSummary, logger)
if err != nil {
return clusterSummary, false, err
}

currentClusterSummary := &configv1beta1.ClusterSummary{}
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
err = c.Get(ctx,
Expand Down Expand Up @@ -2587,7 +2622,8 @@ func updateStatusForReferencedHelmReleases(ctx context.Context, c client.Client,
ReleaseNamespace: instantiatedChart.ReleaseNamespace,
Status: configv1beta1.HelmChartStatusManaging,
FailureMessage: getFailureMessageFromHelmChartSummary(instantiatedChart, clusterSummary),
ValuesHash: getValueHashFromHelmChartSummary(instantiatedChart, clusterSummary), // if a value is currently stored, keep it.
PatchesHash: []byte(patchesHash),
ValuesHash: getValuesHashFromHelmChartSummary(instantiatedChart, clusterSummary), // if a value is currently stored, keep it.
// after chart is deployed such value will be updated
}
currentlyReferenced[helmInfo(instantiatedChart.ReleaseNamespace, instantiatedChart.ReleaseName)] = true
Expand Down Expand Up @@ -3868,6 +3904,19 @@ func updateValueHashOnHelmChartSummary(ctx context.Context, requestedChart *conf
return helmChartValuesHash, err
}

func getHelmChartPatchesHash(ctx context.Context, clusterSummary *configv1beta1.ClusterSummary,
logger logr.Logger) ([]byte, error) {

patchesHash, err := getPatchesHash(ctx, clusterSummary, logger)
if err != nil {
return nil, err
}

h := sha256.New()
h.Write([]byte(patchesHash))
return h.Sum(nil), nil
}

func getCredentialsAndCAFiles(ctx context.Context, c client.Client, clusterSummary *configv1beta1.ClusterSummary,
requestedChart *configv1beta1.HelmChart) (credentialsPath, caPath string, err error) {

Expand Down
5 changes: 5 additions & 0 deletions manifest/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6054,6 +6054,11 @@ spec:
description: FailureMessage provides the specific error from
the Helm engine for this release
type: string
patchesHash:
description: PatchesHash represents of a unique value for the
patches section
format: byte
type: string
releaseName:
description: ReleaseName is the chart release
minLength: 1
Expand Down
164 changes: 164 additions & 0 deletions test/fv/helm_patches_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
Copyright 2026. projectsveltos.io. All rights reserved.

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 fv_test

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"

configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1"
"github.com/projectsveltos/addon-controller/lib/clusterops"
libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
)

var _ = Describe("Helm with patches", func() {
const (
namePrefix = "helm-patches-"
)

It("Deploy and updates helm charts with patches correctly", Label("FV", "FV-PULLMODE"), func() {
Byf("Create a ClusterProfile matching Cluster %s/%s", kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName())
clusterProfile := getClusterProfile(namePrefix, map[string]string{key: value})
clusterProfile.Spec.SyncMode = configv1beta1.SyncModeContinuous
Expect(k8sClient.Create(context.TODO(), clusterProfile)).To(Succeed())

verifyClusterProfileMatches(clusterProfile)

verifyClusterSummary(clusterops.ClusterProfileLabelName,
clusterProfile.Name, &clusterProfile.Spec, kindWorkloadCluster.GetNamespace(),
kindWorkloadCluster.GetName(), getClusterType())

Byf("Update ClusterProfile %s to deploy helm charts", clusterProfile.Name)
currentClusterProfile := &configv1beta1.ClusterProfile{}

err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
Expect(k8sClient.Get(context.TODO(),
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())
currentClusterProfile.Spec.HelmCharts = []configv1beta1.HelmChart{
{
RepositoryURL: "https://argoproj.github.io/argo-helm",
RepositoryName: "argo",
ChartName: "argo/argo-cd",
ChartVersion: "3.35.4",
ReleaseName: "argocd",
ReleaseNamespace: "argocd",
HelmChartAction: configv1beta1.HelmChartActionInstall,
},
}

currentClusterProfile.Spec.Patches = []libsveltosv1beta1.Patch{
{
Target: &libsveltosv1beta1.PatchSelector{
Kind: "Deployment",
Group: "apps",
Version: "v1",
},
Patch: `- op: add
path: /metadata/annotations/test
value: ok`,
},
}
return k8sClient.Update(context.TODO(), currentClusterProfile)
})
Expect(err).To(BeNil())

Expect(k8sClient.Get(context.TODO(),
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())

clusterSummary := verifyClusterSummary(clusterops.ClusterProfileLabelName,
currentClusterProfile.Name, &currentClusterProfile.Spec,
kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName(), getClusterType())

Byf("Getting client to access the workload cluster")
workloadClient, err := getKindWorkloadClusterKubeconfig()
Expect(err).To(BeNil())
Expect(workloadClient).ToNot(BeNil())

Byf("Verifying argocd deployment is created in the workload cluster")
Eventually(func() bool {
depl := &appsv1.Deployment{}
err = workloadClient.Get(context.TODO(),
types.NamespacedName{Namespace: "argocd", Name: "argocd-server"}, depl)
if err != nil {
return false
}
if len(depl.Annotations) == 0 {
return false
}
return depl.Annotations["test"] == "ok"
}, timeout, pollingInterval).Should(BeTrue())

charts := []configv1beta1.Chart{
{ReleaseName: "argocd", ChartVersion: "3.35.4", Namespace: "argocd"},
}

verifyClusterConfiguration(configv1beta1.ClusterProfileKind, clusterProfile.Name,
clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, libsveltosv1beta1.FeatureHelm,
nil, charts)

Byf("Update ClusterProfile %s patches", clusterProfile.Name)

err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
Expect(k8sClient.Get(context.TODO(),
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())
currentClusterProfile.Spec.Patches = []libsveltosv1beta1.Patch{
{
Target: &libsveltosv1beta1.PatchSelector{
Kind: "Deployment",
Group: "apps",
Version: "v1",
},
Patch: `- op: add
path: /metadata/annotations/test2
value: ok2`,
},
}

return k8sClient.Update(context.TODO(), currentClusterProfile)
})
Expect(err).To(BeNil())

Expect(k8sClient.Get(context.TODO(),
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())

verifyClusterSummary(clusterops.ClusterProfileLabelName,
currentClusterProfile.Name, &currentClusterProfile.Spec,
kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName(), getClusterType())

Byf("Verifying argocd deployment is updated in the workload cluster")
Eventually(func() bool {
depl := &appsv1.Deployment{}
err = workloadClient.Get(context.TODO(),
types.NamespacedName{Namespace: "argocd", Name: "argocd-server"}, depl)
if err != nil {
return false
}
if len(depl.Annotations) == 0 {
return false
}
return depl.Annotations["test2"] == "ok2"
}, timeout, pollingInterval).Should(BeTrue())

deleteClusterProfile(clusterProfile)
})
})