Skip to content

Commit 725b5c7

Browse files
authored
Merge pull request #1686 from gianlucam76/helm-patches
(bug) Helm deployment: react to patches changes
2 parents 6139402 + f4a9b72 commit 725b5c7

6 files changed

Lines changed: 236 additions & 4 deletions

File tree

api/v1beta1/clustersummary_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ type HelmChartSummary struct {
110110
// +optional
111111
ValuesHash []byte `json:"valuesHash,omitempty"`
112112

113+
// PatchesHash represents of a unique value for the patches section
114+
// +optional
115+
PatchesHash []byte `json:"patchesHash,omitempty"`
116+
113117
// Status indicates whether ClusterSummary can manage the helm
114118
// chart or there is a conflict
115119
// +optional

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/config.projectsveltos.io_clustersummaries.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,6 +1674,11 @@ spec:
16741674
description: FailureMessage provides the specific error from
16751675
the Helm engine for this release
16761676
type: string
1677+
patchesHash:
1678+
description: PatchesHash represents of a unique value for the
1679+
patches section
1680+
format: byte
1681+
type: string
16771682
releaseName:
16781683
description: ReleaseName is the chart release
16791684
minLength: 1

controllers/handlers_helm.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,9 +1105,9 @@ func getFailureMessageFromHelmChartSummary(requestedChart *configv1beta1.HelmCha
11051105
return nil
11061106
}
11071107

1108-
// getValueHashFromHelmChartSummary returns the valueHash stored for this chart
1108+
// getValuesHashFromHelmChartSummary returns the valueHash stored for this chart
11091109
// in the ClusterSummary
1110-
func getValueHashFromHelmChartSummary(requestedChart *configv1beta1.HelmChart,
1110+
func getValuesHashFromHelmChartSummary(requestedChart *configv1beta1.HelmChart,
11111111
clusterSummary *configv1beta1.ClusterSummary) []byte {
11121112

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

1125+
// getPatchesHashFromHelmChartSummary returns the patchesHash stored for this chart
1126+
// in the ClusterSummary
1127+
func getPatchesHashFromHelmChartSummary(requestedChart *configv1beta1.HelmChart,
1128+
clusterSummary *configv1beta1.ClusterSummary) []byte {
1129+
1130+
for i := range clusterSummary.Status.HelmReleaseSummaries {
1131+
rs := &clusterSummary.Status.HelmReleaseSummaries[i]
1132+
if rs.ReleaseName == requestedChart.ReleaseName &&
1133+
rs.ReleaseNamespace == requestedChart.ReleaseNamespace {
1134+
1135+
return rs.PatchesHash
1136+
}
1137+
}
1138+
1139+
return nil
1140+
}
1141+
11251142
func generateConflictForHelmChart(ctx context.Context, clusterSummary *configv1beta1.ClusterSummary,
11261143
instantiatedChart *configv1beta1.HelmChart) string {
11271144

@@ -2297,10 +2314,18 @@ func shouldUpgrade(ctx context.Context, currentRelease *releaseInfo, instantiate
22972314
return false
22982315
}
22992316

2317+
currentPatchesHash, err := getHelmChartPatchesHash(ctx, clusterSummary, logger)
2318+
if err != nil {
2319+
logger.V(logs.LogInfo).Info(fmt.Sprintf("failed to get current patches hash: %v", err))
2320+
currentPatchesHash = []byte("")
2321+
}
2322+
23002323
if clusterSummary.Spec.ClusterProfileSpec.SyncMode != configv1beta1.SyncModeContinuousWithDriftDetection {
23012324
if clusterSummary.Spec.ClusterProfileSpec.SyncMode != configv1beta1.SyncModeDryRun {
2302-
oldValueHash := getValueHashFromHelmChartSummary(instantiatedChart, clusterSummary)
2325+
oldValueHash := getValuesHashFromHelmChartSummary(instantiatedChart, clusterSummary)
2326+
oldPatchesHash := getPatchesHashFromHelmChartSummary(instantiatedChart, clusterSummary)
23032327

2328+
// Compare Values
23042329
c := getManagementClusterClient()
23052330
currentValueHash, err := getHelmChartValuesHash(ctx, c, instantiatedChart, clusterSummary, mgmtResources, logger)
23062331
if err != nil {
@@ -2310,6 +2335,11 @@ func shouldUpgrade(ctx context.Context, currentRelease *releaseInfo, instantiate
23102335
if !reflect.DeepEqual(oldValueHash, currentValueHash) {
23112336
return true
23122337
}
2338+
2339+
// Compare patches
2340+
if !reflect.DeepEqual(oldPatchesHash, currentPatchesHash) {
2341+
return true
2342+
}
23132343
}
23142344

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

25612591
conflict := false
25622592

2593+
patchesHash, err := getPatchesHash(ctx, clusterSummary, logger)
2594+
if err != nil {
2595+
return clusterSummary, false, err
2596+
}
2597+
25632598
currentClusterSummary := &configv1beta1.ClusterSummary{}
25642599
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
25652600
err = c.Get(ctx,
@@ -2587,7 +2622,8 @@ func updateStatusForReferencedHelmReleases(ctx context.Context, c client.Client,
25872622
ReleaseNamespace: instantiatedChart.ReleaseNamespace,
25882623
Status: configv1beta1.HelmChartStatusManaging,
25892624
FailureMessage: getFailureMessageFromHelmChartSummary(instantiatedChart, clusterSummary),
2590-
ValuesHash: getValueHashFromHelmChartSummary(instantiatedChart, clusterSummary), // if a value is currently stored, keep it.
2625+
PatchesHash: []byte(patchesHash),
2626+
ValuesHash: getValuesHashFromHelmChartSummary(instantiatedChart, clusterSummary), // if a value is currently stored, keep it.
25912627
// after chart is deployed such value will be updated
25922628
}
25932629
currentlyReferenced[helmInfo(instantiatedChart.ReleaseNamespace, instantiatedChart.ReleaseName)] = true
@@ -3868,6 +3904,19 @@ func updateValueHashOnHelmChartSummary(ctx context.Context, requestedChart *conf
38683904
return helmChartValuesHash, err
38693905
}
38703906

3907+
func getHelmChartPatchesHash(ctx context.Context, clusterSummary *configv1beta1.ClusterSummary,
3908+
logger logr.Logger) ([]byte, error) {
3909+
3910+
patchesHash, err := getPatchesHash(ctx, clusterSummary, logger)
3911+
if err != nil {
3912+
return nil, err
3913+
}
3914+
3915+
h := sha256.New()
3916+
h.Write([]byte(patchesHash))
3917+
return h.Sum(nil), nil
3918+
}
3919+
38713920
func getCredentialsAndCAFiles(ctx context.Context, c client.Client, clusterSummary *configv1beta1.ClusterSummary,
38723921
requestedChart *configv1beta1.HelmChart) (credentialsPath, caPath string, err error) {
38733922

manifest/manifest.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6054,6 +6054,11 @@ spec:
60546054
description: FailureMessage provides the specific error from
60556055
the Helm engine for this release
60566056
type: string
6057+
patchesHash:
6058+
description: PatchesHash represents of a unique value for the
6059+
patches section
6060+
format: byte
6061+
type: string
60576062
releaseName:
60586063
description: ReleaseName is the chart release
60596064
minLength: 1

test/fv/helm_patches_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
Copyright 2026. projectsveltos.io. All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package fv_test
18+
19+
import (
20+
"context"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
25+
appsv1 "k8s.io/api/apps/v1"
26+
"k8s.io/apimachinery/pkg/types"
27+
"k8s.io/client-go/util/retry"
28+
29+
configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1"
30+
"github.com/projectsveltos/addon-controller/lib/clusterops"
31+
libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
32+
)
33+
34+
var _ = Describe("Helm with patches", func() {
35+
const (
36+
namePrefix = "helm-patches-"
37+
)
38+
39+
It("Deploy and updates helm charts with patches correctly", Label("FV", "FV-PULLMODE"), func() {
40+
Byf("Create a ClusterProfile matching Cluster %s/%s", kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName())
41+
clusterProfile := getClusterProfile(namePrefix, map[string]string{key: value})
42+
clusterProfile.Spec.SyncMode = configv1beta1.SyncModeContinuous
43+
Expect(k8sClient.Create(context.TODO(), clusterProfile)).To(Succeed())
44+
45+
verifyClusterProfileMatches(clusterProfile)
46+
47+
verifyClusterSummary(clusterops.ClusterProfileLabelName,
48+
clusterProfile.Name, &clusterProfile.Spec, kindWorkloadCluster.GetNamespace(),
49+
kindWorkloadCluster.GetName(), getClusterType())
50+
51+
Byf("Update ClusterProfile %s to deploy helm charts", clusterProfile.Name)
52+
currentClusterProfile := &configv1beta1.ClusterProfile{}
53+
54+
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
55+
Expect(k8sClient.Get(context.TODO(),
56+
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())
57+
currentClusterProfile.Spec.HelmCharts = []configv1beta1.HelmChart{
58+
{
59+
RepositoryURL: "https://argoproj.github.io/argo-helm",
60+
RepositoryName: "argo",
61+
ChartName: "argo/argo-cd",
62+
ChartVersion: "3.35.4",
63+
ReleaseName: "argocd",
64+
ReleaseNamespace: "argocd",
65+
HelmChartAction: configv1beta1.HelmChartActionInstall,
66+
},
67+
}
68+
69+
currentClusterProfile.Spec.Patches = []libsveltosv1beta1.Patch{
70+
{
71+
Target: &libsveltosv1beta1.PatchSelector{
72+
Kind: "Deployment",
73+
Group: "apps",
74+
Version: "v1",
75+
},
76+
Patch: `- op: add
77+
path: /metadata/annotations/test
78+
value: ok`,
79+
},
80+
}
81+
return k8sClient.Update(context.TODO(), currentClusterProfile)
82+
})
83+
Expect(err).To(BeNil())
84+
85+
Expect(k8sClient.Get(context.TODO(),
86+
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())
87+
88+
clusterSummary := verifyClusterSummary(clusterops.ClusterProfileLabelName,
89+
currentClusterProfile.Name, &currentClusterProfile.Spec,
90+
kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName(), getClusterType())
91+
92+
Byf("Getting client to access the workload cluster")
93+
workloadClient, err := getKindWorkloadClusterKubeconfig()
94+
Expect(err).To(BeNil())
95+
Expect(workloadClient).ToNot(BeNil())
96+
97+
Byf("Verifying argocd deployment is created in the workload cluster")
98+
Eventually(func() bool {
99+
depl := &appsv1.Deployment{}
100+
err = workloadClient.Get(context.TODO(),
101+
types.NamespacedName{Namespace: "argocd", Name: "argocd-server"}, depl)
102+
if err != nil {
103+
return false
104+
}
105+
if len(depl.Annotations) == 0 {
106+
return false
107+
}
108+
return depl.Annotations["test"] == "ok"
109+
}, timeout, pollingInterval).Should(BeTrue())
110+
111+
charts := []configv1beta1.Chart{
112+
{ReleaseName: "argocd", ChartVersion: "3.35.4", Namespace: "argocd"},
113+
}
114+
115+
verifyClusterConfiguration(configv1beta1.ClusterProfileKind, clusterProfile.Name,
116+
clusterSummary.Spec.ClusterNamespace, clusterSummary.Spec.ClusterName, libsveltosv1beta1.FeatureHelm,
117+
nil, charts)
118+
119+
Byf("Update ClusterProfile %s patches", clusterProfile.Name)
120+
121+
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
122+
Expect(k8sClient.Get(context.TODO(),
123+
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())
124+
currentClusterProfile.Spec.Patches = []libsveltosv1beta1.Patch{
125+
{
126+
Target: &libsveltosv1beta1.PatchSelector{
127+
Kind: "Deployment",
128+
Group: "apps",
129+
Version: "v1",
130+
},
131+
Patch: `- op: add
132+
path: /metadata/annotations/test2
133+
value: ok2`,
134+
},
135+
}
136+
137+
return k8sClient.Update(context.TODO(), currentClusterProfile)
138+
})
139+
Expect(err).To(BeNil())
140+
141+
Expect(k8sClient.Get(context.TODO(),
142+
types.NamespacedName{Name: clusterProfile.Name}, currentClusterProfile)).To(Succeed())
143+
144+
verifyClusterSummary(clusterops.ClusterProfileLabelName,
145+
currentClusterProfile.Name, &currentClusterProfile.Spec,
146+
kindWorkloadCluster.GetNamespace(), kindWorkloadCluster.GetName(), getClusterType())
147+
148+
Byf("Verifying argocd deployment is updated in the workload cluster")
149+
Eventually(func() bool {
150+
depl := &appsv1.Deployment{}
151+
err = workloadClient.Get(context.TODO(),
152+
types.NamespacedName{Namespace: "argocd", Name: "argocd-server"}, depl)
153+
if err != nil {
154+
return false
155+
}
156+
if len(depl.Annotations) == 0 {
157+
return false
158+
}
159+
return depl.Annotations["test2"] == "ok2"
160+
}, timeout, pollingInterval).Should(BeTrue())
161+
162+
deleteClusterProfile(clusterProfile)
163+
})
164+
})

0 commit comments

Comments
 (0)