@@ -5,6 +5,7 @@ package addons
5
5
6
6
import (
7
7
"context"
8
+ "crypto/sha256"
8
9
"fmt"
9
10
10
11
"github.com/go-logr/logr"
@@ -20,6 +21,11 @@ import (
20
21
handlersutils "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/utils"
21
22
)
22
23
24
+ var (
25
+ HelmReleaseNameHashLabel = "addons.cluster.x-k8s.io/helm-release-name-hash"
26
+ ClusterNamespaceLabel = clusterv1 .ClusterNamespaceAnnotation
27
+ )
28
+
23
29
type HelmAddonConfig struct {
24
30
defaultValuesTemplateConfigMapName string
25
31
@@ -52,6 +58,7 @@ type helmAddonApplier struct {
52
58
config * HelmAddonConfig
53
59
client ctrlclient.Client
54
60
helmChart * lifecycleconfig.HelmChart
61
+ opts []applyOption
55
62
}
56
63
57
64
var _ Applier = & helmAddonApplier {}
@@ -68,12 +75,48 @@ func NewHelmAddonApplier(
68
75
}
69
76
}
70
77
78
+ type applyOptions struct {
79
+ valueTemplater func (cluster * clusterv1.Cluster , valuesTemplate string ) (string , error )
80
+ targetCluster * clusterv1.Cluster
81
+ }
82
+
83
+ type applyOption func (* applyOptions )
84
+
85
+ func (a * helmAddonApplier ) WithValueTemplater (
86
+ valueTemplater func (cluster * clusterv1.Cluster , valuesTemplate string ) (string , error ),
87
+ ) * helmAddonApplier {
88
+ a .opts = append (a .opts , func (o * applyOptions ) {
89
+ o .valueTemplater = valueTemplater
90
+ })
91
+
92
+ return a
93
+ }
94
+
95
+ func (a * helmAddonApplier ) WithTargetCluster (cluster * clusterv1.Cluster ) * helmAddonApplier {
96
+ a .opts = append (a .opts , func (o * applyOptions ) {
97
+ o .targetCluster = cluster
98
+ })
99
+
100
+ return a
101
+ }
102
+
71
103
func (a * helmAddonApplier ) Apply (
72
104
ctx context.Context ,
73
105
cluster * clusterv1.Cluster ,
74
106
defaultsNamespace string ,
75
107
log logr.Logger ,
76
108
) error {
109
+ applyOpts := & applyOptions {}
110
+ for _ , opt := range a .opts {
111
+ opt (applyOpts )
112
+ }
113
+
114
+ log .Info ("Checking for existing HelmChartProxy for cluster" )
115
+ chartProxy , err := a .FindExistingHelmChartProxy (ctx , cluster )
116
+ if err != nil {
117
+ return fmt .Errorf ("failed to lookup existing HelmChartProxy for cluster: %w" , err )
118
+ }
119
+
77
120
log .Info ("Retrieving installation values template for cluster" )
78
121
values , err := handlersutils .RetrieveValuesTemplate (
79
122
ctx ,
@@ -88,39 +131,115 @@ func (a *helmAddonApplier) Apply(
88
131
)
89
132
}
90
133
91
- chartProxy := & caaphv1.HelmChartProxy {
92
- TypeMeta : metav1.TypeMeta {
93
- APIVersion : caaphv1 .GroupVersion .String (),
94
- Kind : "HelmChartProxy" ,
95
- },
96
- ObjectMeta : metav1.ObjectMeta {
97
- Namespace : cluster .Namespace ,
98
- Name : a .config .defaultHelmReleaseName + "-" + cluster .Name ,
99
- },
100
- Spec : caaphv1.HelmChartProxySpec {
101
- RepoURL : a .helmChart .Repository ,
102
- ChartName : a .helmChart .Name ,
103
- ClusterSelector : metav1.LabelSelector {
104
- MatchLabels : map [string ]string {clusterv1 .ClusterNameLabel : cluster .Name },
134
+ if applyOpts .valueTemplater != nil {
135
+ values , err = applyOpts .valueTemplater (cluster , values )
136
+ if err != nil {
137
+ return fmt .Errorf ("failed to template Helm values: %w" , err )
138
+ }
139
+ }
140
+
141
+ targetCluster := cluster
142
+ if applyOpts .targetCluster != nil {
143
+ targetCluster = applyOpts .targetCluster
144
+ }
145
+
146
+ if chartProxy == nil {
147
+ chartProxy = & caaphv1.HelmChartProxy {
148
+ TypeMeta : metav1.TypeMeta {
149
+ APIVersion : caaphv1 .GroupVersion .String (),
150
+ Kind : "HelmChartProxy" ,
151
+ },
152
+ ObjectMeta : metav1.ObjectMeta {
153
+ Namespace : targetCluster .Namespace ,
154
+ Labels : map [string ]string {
155
+ clusterv1 .ClusterNameLabel : cluster .Name ,
156
+ ClusterNamespaceLabel : cluster .Namespace ,
157
+ // Label values have a maximum length of 63 characters so hash the release name to
158
+ // ensure it fits within the limit.
159
+ HelmReleaseNameHashLabel : hashReleaseName (a .config .defaultHelmReleaseName ),
160
+ },
161
+ GenerateName : fmt .Sprintf ("%s-" , a .config .defaultHelmReleaseName ),
105
162
},
106
- ReleaseNamespace : a .config .defaultHelmReleaseNamespace ,
107
- ReleaseName : a .config .defaultHelmReleaseName ,
108
- Version : a .helmChart .Version ,
109
- ValuesTemplate : values ,
163
+ }
164
+ }
165
+
166
+ chartProxy .Spec = caaphv1.HelmChartProxySpec {
167
+ RepoURL : a .helmChart .Repository ,
168
+ ChartName : a .helmChart .Name ,
169
+ ClusterSelector : metav1.LabelSelector {
170
+ MatchLabels : map [string ]string {clusterv1 .ClusterNameLabel : targetCluster .Name },
110
171
},
172
+ ReleaseNamespace : a .config .defaultHelmReleaseNamespace ,
173
+ ReleaseName : a .config .defaultHelmReleaseName ,
174
+ Version : a .helmChart .Version ,
175
+ ValuesTemplate : values ,
111
176
}
177
+
112
178
handlersutils .SetTLSConfigForHelmChartProxyIfNeeded (chartProxy )
113
- if err = controllerutil .SetOwnerReference (cluster , chartProxy , a .client .Scheme ()); err != nil {
179
+ if err = controllerutil .SetOwnerReference (targetCluster , chartProxy , a .client .Scheme ()); err != nil {
114
180
return fmt .Errorf (
115
181
"failed to set owner reference on HelmChartProxy %q: %w" ,
116
182
chartProxy .Name ,
117
183
err ,
118
184
)
119
185
}
120
186
121
- if err = k8sclient .ServerSideApply (ctx , a .client , chartProxy , k8sclient .ForceOwnership ); err != nil {
122
- return fmt .Errorf ("failed to apply HelmChartProxy %q: %w" , chartProxy .Name , err )
187
+ // Only server-side apply the HelmChartProxy if it already existed, i.e. that metadata.name is non-empty.
188
+ // This allows to use metadata.generateName for the first creation to avoid naming collisions.
189
+ if chartProxy .Name == "" {
190
+ if err = a .client .Create (ctx , chartProxy ); err != nil {
191
+ return fmt .Errorf ("failed to create HelmChartProxy %q: %w" , chartProxy .Name , err )
192
+ }
193
+ } else {
194
+ // metadata.managedFields must be nil when using server-side apply.
195
+ chartProxy .ManagedFields = nil
196
+ if err = k8sclient .ServerSideApply (ctx , a .client , chartProxy , k8sclient .ForceOwnership ); err != nil {
197
+ return fmt .Errorf ("failed to apply HelmChartProxy %q: %w" , chartProxy .Name , err )
198
+ }
123
199
}
124
200
125
201
return nil
126
202
}
203
+
204
+ func (a * helmAddonApplier ) FindExistingHelmChartProxy (
205
+ ctx context.Context , cluster * clusterv1.Cluster ,
206
+ ) (* caaphv1.HelmChartProxy , error ) {
207
+ applyOpts := & applyOptions {}
208
+ for _ , opt := range a .opts {
209
+ opt (applyOpts )
210
+ }
211
+
212
+ targetCluster := cluster
213
+ if applyOpts .targetCluster != nil {
214
+ targetCluster = applyOpts .targetCluster
215
+ }
216
+
217
+ chartProxyList := & caaphv1.HelmChartProxyList {}
218
+ if err := a .client .List (
219
+ ctx ,
220
+ chartProxyList ,
221
+ ctrlclient.MatchingLabels {
222
+ clusterv1 .ClusterNameLabel : cluster .Name ,
223
+ ClusterNamespaceLabel : cluster .Namespace ,
224
+ HelmReleaseNameHashLabel : hashReleaseName (a .config .defaultHelmReleaseName ),
225
+ },
226
+ ctrlclient .InNamespace (targetCluster .Namespace ),
227
+ ); err != nil {
228
+ return nil , fmt .Errorf ("failed to list HelmChartProxies: %w" , err )
229
+ }
230
+
231
+ if len (chartProxyList .Items ) == 0 {
232
+ return nil , nil
233
+ }
234
+
235
+ if len (chartProxyList .Items ) > 1 {
236
+ return nil , fmt .Errorf ("found multiple HelmChartProxies for cluster %q" , cluster .Name )
237
+ }
238
+
239
+ return & chartProxyList .Items [0 ], nil
240
+ }
241
+
242
+ func hashReleaseName (releaseName string ) string {
243
+ // Use Sum224 to ensure the hash is 56 characters long.
244
+ return fmt .Sprintf ("%x" , sha256 .Sum224 ([]byte (releaseName )))
245
+ }
0 commit comments