Skip to content

Commit effb100

Browse files
committed
fix: Handle long cluster names
This commit starts fixing a bug that means addons fail to be fully deployed if the cluster name is longer than 44 characters. This is caused by the name of the HCP being over 63 characters. This name is then used in HRP labels which have a maximum length of 63 characters, so the HRPs are rejected by the API server when CAAPH applies them. The fix is to use generate name and labels on the HCP to ensure uniqueness by lookup rather than by using a deterministic name.
1 parent bd575b7 commit effb100

File tree

16 files changed

+346
-427
lines changed

16 files changed

+346
-427
lines changed

pkg/handlers/generic/lifecycle/addons/helmaddon.go

Lines changed: 140 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package addons
55

66
import (
77
"context"
8+
"crypto/sha256"
89
"fmt"
910

1011
"github.com/go-logr/logr"
@@ -20,6 +21,11 @@ import (
2021
handlersutils "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/utils"
2122
)
2223

24+
var (
25+
HelmReleaseNameHashLabel = "addons.cluster.x-k8s.io/helm-release-name-hash"
26+
ClusterNamespaceLabel = clusterv1.ClusterNamespaceAnnotation
27+
)
28+
2329
type HelmAddonConfig struct {
2430
defaultValuesTemplateConfigMapName string
2531

@@ -52,6 +58,7 @@ type helmAddonApplier struct {
5258
config *HelmAddonConfig
5359
client ctrlclient.Client
5460
helmChart *lifecycleconfig.HelmChart
61+
opts []applyOption
5562
}
5663

5764
var _ Applier = &helmAddonApplier{}
@@ -68,12 +75,48 @@ func NewHelmAddonApplier(
6875
}
6976
}
7077

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+
71103
func (a *helmAddonApplier) Apply(
72104
ctx context.Context,
73105
cluster *clusterv1.Cluster,
74106
defaultsNamespace string,
75107
log logr.Logger,
76108
) 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+
77120
log.Info("Retrieving installation values template for cluster")
78121
values, err := handlersutils.RetrieveValuesTemplate(
79122
ctx,
@@ -88,39 +131,115 @@ func (a *helmAddonApplier) Apply(
88131
)
89132
}
90133

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),
105162
},
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},
110171
},
172+
ReleaseNamespace: a.config.defaultHelmReleaseNamespace,
173+
ReleaseName: a.config.defaultHelmReleaseName,
174+
Version: a.helmChart.Version,
175+
ValuesTemplate: values,
111176
}
177+
112178
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 {
114180
return fmt.Errorf(
115181
"failed to set owner reference on HelmChartProxy %q: %w",
116182
chartProxy.Name,
117183
err,
118184
)
119185
}
120186

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+
}
123199
}
124200

125201
return nil
126202
}
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+
}

pkg/handlers/generic/lifecycle/ccm/nutanix/handler.go

Lines changed: 43 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ import (
1212

1313
"github.com/go-logr/logr"
1414
"github.com/spf13/pflag"
15-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1615
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1716
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
18-
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1917

20-
caaphv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
2118
apivariables "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables"
22-
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/k8s/client"
19+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/addons"
2320
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/config"
2421
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
2522
handlersutils "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/utils"
@@ -80,25 +77,11 @@ func (p *provider) Apply(
8077
return ErrMissingCredentials
8178
}
8279

83-
log.Info("Retrieving Nutanix CCM installation values template for cluster")
84-
values, err := handlersutils.RetrieveValuesTemplate(
85-
ctx,
86-
p.client,
87-
p.config.defaultValuesTemplateConfigMapName,
88-
p.config.DefaultsNamespace(),
89-
)
90-
if err != nil {
91-
return fmt.Errorf(
92-
"failed to retrieve Nutanix CCM installation values template for cluster: %w",
93-
err,
94-
)
95-
}
96-
9780
// It's possible to have the credentials Secret be created by the Helm chart.
9881
// However, that would leave the credentials visible in the HelmChartProxy.
9982
// Instead, we'll create the Secret on the remote cluster and reference it in the Helm values.
10083
if clusterConfig.Addons.CCM.Credentials != nil {
101-
err = handlersutils.EnsureOwnerReferenceForSecret(
84+
err := handlersutils.EnsureOwnerReferenceForSecret(
10285
ctx,
10386
p.client,
10487
clusterConfig.Addons.CCM.Credentials.SecretRef.Name,
@@ -134,77 +117,56 @@ func (p *provider) Apply(
134117
return fmt.Errorf("failed to get values for nutanix-ccm-config: %w", err)
135118
}
136119

137-
// The configMap will contain the Helm values, but templated with fields that need to be filled in.
138-
values, err = templateValues(clusterConfig, values)
139-
if err != nil {
140-
return fmt.Errorf("failed to template Helm values read from ConfigMap: %w", err)
141-
}
142-
143-
hcp := &caaphv1.HelmChartProxy{
144-
TypeMeta: metav1.TypeMeta{
145-
APIVersion: caaphv1.GroupVersion.String(),
146-
Kind: "HelmChartProxy",
147-
},
148-
ObjectMeta: metav1.ObjectMeta{
149-
Namespace: cluster.Namespace,
150-
Name: "nutanix-ccm-" + cluster.Name,
151-
},
152-
Spec: caaphv1.HelmChartProxySpec{
153-
RepoURL: helmChart.Repository,
154-
ChartName: helmChart.Name,
155-
ClusterSelector: metav1.LabelSelector{
156-
MatchLabels: map[string]string{clusterv1.ClusterNameLabel: cluster.Name},
157-
},
158-
ReleaseNamespace: defaultHelmReleaseNamespace,
159-
ReleaseName: defaultHelmReleaseName,
160-
Version: helmChart.Version,
161-
ValuesTemplate: values,
162-
},
163-
}
164-
handlersutils.SetTLSConfigForHelmChartProxyIfNeeded(hcp)
165-
if err = controllerutil.SetOwnerReference(cluster, hcp, p.client.Scheme()); err != nil {
166-
return fmt.Errorf(
167-
"failed to set owner reference on nutanix-ccm installation HelmChartProxy: %w",
168-
err,
169-
)
170-
}
120+
applier := addons.NewHelmAddonApplier(
121+
addons.NewHelmAddonConfig(
122+
p.config.defaultValuesTemplateConfigMapName,
123+
defaultHelmReleaseNamespace,
124+
defaultHelmReleaseName,
125+
),
126+
p.client,
127+
helmChart,
128+
).WithValueTemplater(templateValuesFunc(clusterConfig))
171129

172-
if err = client.ServerSideApply(ctx, p.client, hcp, client.ForceOwnership); err != nil {
130+
if err = applier.Apply(ctx, cluster, p.config.DefaultsNamespace(), log); err != nil {
173131
return fmt.Errorf("failed to apply nutanix-ccm installation HelmChartProxy: %w", err)
174132
}
175133

176134
return nil
177135
}
178136

179-
func templateValues(clusterConfig *apivariables.ClusterConfigSpec, text string) (string, error) {
180-
helmValuesTemplate, err := template.New("").Parse(text)
181-
if err != nil {
182-
return "", fmt.Errorf("failed to parse Helm values template: %w", err)
183-
}
137+
func templateValuesFunc(
138+
clusterConfig *apivariables.ClusterConfigSpec,
139+
) func(*clusterv1.Cluster, string) (string, error) {
140+
return func(_ *clusterv1.Cluster, valuesTemplate string) (string, error) {
141+
helmValuesTemplate, err := template.New("").Parse(valuesTemplate)
142+
if err != nil {
143+
return "", fmt.Errorf("failed to parse Helm values template: %w", err)
144+
}
184145

185-
type input struct {
186-
PrismCentralHost string
187-
PrismCentralPort int32
188-
PrismCentralInsecure bool
189-
PrismCentralAdditionalTrustBundle string
190-
}
146+
type input struct {
147+
PrismCentralHost string
148+
PrismCentralPort int32
149+
PrismCentralInsecure bool
150+
PrismCentralAdditionalTrustBundle string
151+
}
191152

192-
address, port, err := clusterConfig.Nutanix.PrismCentralEndpoint.ParseURL()
193-
if err != nil {
194-
return "", err
195-
}
196-
templateInput := input{
197-
PrismCentralHost: address,
198-
PrismCentralPort: port,
199-
PrismCentralInsecure: clusterConfig.Nutanix.PrismCentralEndpoint.Insecure,
200-
PrismCentralAdditionalTrustBundle: clusterConfig.Nutanix.PrismCentralEndpoint.AdditionalTrustBundle,
201-
}
153+
address, port, err := clusterConfig.Nutanix.PrismCentralEndpoint.ParseURL()
154+
if err != nil {
155+
return "", err
156+
}
157+
templateInput := input{
158+
PrismCentralHost: address,
159+
PrismCentralPort: port,
160+
PrismCentralInsecure: clusterConfig.Nutanix.PrismCentralEndpoint.Insecure,
161+
PrismCentralAdditionalTrustBundle: clusterConfig.Nutanix.PrismCentralEndpoint.AdditionalTrustBundle,
162+
}
202163

203-
var b bytes.Buffer
204-
err = helmValuesTemplate.Execute(&b, templateInput)
205-
if err != nil {
206-
return "", fmt.Errorf("failed setting PrismCentral configuration in template: %w", err)
207-
}
164+
var b bytes.Buffer
165+
err = helmValuesTemplate.Execute(&b, templateInput)
166+
if err != nil {
167+
return "", fmt.Errorf("failed setting PrismCentral configuration in template: %w", err)
168+
}
208169

209-
return b.String(), nil
170+
return b.String(), nil
171+
}
210172
}

pkg/handlers/generic/lifecycle/ccm/nutanix/handler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func Test_templateValues(t *testing.T) {
123123
tt := tests[idx]
124124
t.Run(tt.name, func(t *testing.T) {
125125
t.Parallel()
126-
out, err := templateValues(tt.clusterConfig, tt.in)
126+
out, err := templateValuesFunc(tt.clusterConfig)(nil, tt.in)
127127
require.NoError(t, err)
128128
assert.Equal(t, tt.expected, out)
129129
})

0 commit comments

Comments
 (0)