Skip to content
This repository was archived by the owner on Apr 11, 2024. It is now read-only.

Commit 961ee09

Browse files
committed
feat: deploy Nutanix CCM addon
Aligns the method of deploying the CCM with all other addons.
1 parent 130d4ec commit 961ee09

File tree

9 files changed

+360
-8
lines changed

9 files changed

+360
-8
lines changed

api/v1alpha1/addon_types.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,34 @@ func (CSI) VariableSchema() clusterv1.VariableSchema {
311311
}
312312

313313
// CCM tells us to enable or disable the cloud provider interface.
314-
type CCM struct{}
314+
type CCM struct {
315+
// A reference to the Secret for credential information for the target Prism Central instance
316+
// +optional
317+
Credentials *corev1.LocalObjectReference `json:"credentials"`
318+
}
315319

316320
func (CCM) VariableSchema() clusterv1.VariableSchema {
321+
// TODO Validate credentials is set.
322+
// This CCM is shared across all providers.
323+
// Some of these providers may require credentials to be set, but we don't want to require it for all providers.
324+
// The Nutanix CCM handler will fail in at runtime if credentials are not set.
317325
return clusterv1.VariableSchema{
318326
OpenAPIV3Schema: clusterv1.JSONSchemaProps{
319327
Type: "object",
328+
Properties: map[string]clusterv1.JSONSchemaProps{
329+
"credentials": {
330+
Description: "A reference to the Secret for credential information" +
331+
"for the target Prism Central instance",
332+
Type: "object",
333+
Properties: map[string]clusterv1.JSONSchemaProps{
334+
"name": {
335+
Description: "The name of the Secret",
336+
Type: "string",
337+
},
338+
},
339+
Required: []string{"name"},
340+
},
341+
},
320342
},
321343
}
322344
}

api/v1alpha1/clusterconfig_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const (
2525
CSIProviderAWSEBS = "aws-ebs"
2626
CSIProviderNutanix = "nutanix"
2727

28-
CCMProviderAWS = "aws"
28+
CCMProviderAWS = "aws"
29+
CCMProviderNutanix = "nutanix"
2930
)
3031

3132
// +kubebuilder:object:root=true

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
ctrl "sigs.k8s.io/controller-runtime"
1616
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
1717

18+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
1819
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/k8s/client"
1920
lifecycleutils "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/utils"
2021
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
@@ -56,6 +57,7 @@ func New(
5657
func (a *AWSCCM) Apply(
5758
ctx context.Context,
5859
cluster *clusterv1.Cluster,
60+
_ *v1alpha1.ClusterConfigSpec,
5961
) error {
6062
log := ctrl.LoggerFrom(ctx).WithValues(
6163
"cluster",

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const (
2525
)
2626

2727
type CCMProvider interface {
28-
Apply(context.Context, *clusterv1.Cluster) error
28+
Apply(context.Context, *clusterv1.Cluster, *v1alpha1.ClusterConfigSpec) error
2929
}
3030

3131
type CCMHandler struct {
@@ -78,7 +78,7 @@ func (c *CCMHandler) AfterControlPlaneInitialized(
7878
)
7979
resp.SetStatus(runtimehooksv1.ResponseStatusFailure)
8080
resp.SetMessage(
81-
fmt.Sprintf("failed to read CCM provider from cluster definition: %v",
81+
fmt.Sprintf("failed to read CCM from cluster definition: %v",
8282
err,
8383
),
8484
)
@@ -88,17 +88,36 @@ func (c *CCMHandler) AfterControlPlaneInitialized(
8888
log.V(4).Info("Skipping CCM handler.")
8989
return
9090
}
91+
92+
clusterConfigVar, found, err := variables.Get[v1alpha1.ClusterConfigSpec](varMap, clusterconfig.MetaVariableName)
93+
if err != nil {
94+
log.Error(
95+
err,
96+
"failed to read clusterConfig variable from cluster definition",
97+
)
98+
resp.SetStatus(runtimehooksv1.ResponseStatusFailure)
99+
resp.SetMessage(
100+
fmt.Sprintf("ffailed to read clusterConfig variable from cluster definition: %v",
101+
err,
102+
),
103+
)
104+
return
105+
}
106+
91107
infraKind := req.Cluster.Spec.InfrastructureRef.Kind
92108
log.Info(fmt.Sprintf("finding CCM handler for %s", infraKind))
93109
var handler CCMProvider
94110
switch {
95111
case strings.Contains(strings.ToLower(infraKind), v1alpha1.CCMProviderAWS):
96112
handler = c.ProviderHandler[v1alpha1.CCMProviderAWS]
113+
case strings.Contains(strings.ToLower(infraKind), v1alpha1.CCMProviderNutanix):
114+
handler = c.ProviderHandler[v1alpha1.CCMProviderNutanix]
97115
default:
98116
log.Info(fmt.Sprintf("No CCM handler provided for infra kind %s", infraKind))
99117
return
100118
}
101-
err = handler.Apply(ctx, &req.Cluster)
119+
120+
err = handler.Apply(ctx, &req.Cluster, &clusterConfigVar)
102121
if err != nil {
103122
log.Error(
104123
err,
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2023 D2iQ, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package nutanix
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"errors"
10+
"fmt"
11+
"text/template"
12+
13+
"github.com/spf13/pflag"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
16+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
17+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
18+
19+
caaphv1 "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/external/sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
20+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
21+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/k8s/client"
22+
lifecycleutils "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/utils"
23+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
24+
)
25+
26+
const (
27+
defaultHelmRepositoryURL = "https://nutanix.github.io/helm/"
28+
defaultHelmChartVersion = "0.3.3"
29+
defaultHelmChartName = "nutanix-cloud-provider"
30+
defaultHelmReleaseName = "nutanix-ccm"
31+
defaultHelmReleaseNamespace = "kube-system"
32+
33+
// This is the name of the Secret on the remote cluster that should match what is defined in Helm values.
34+
defaultCredentialsSecretName = "nutanix-ccm-credentials"
35+
)
36+
37+
//nolint:gosec // Does not contain hard coded credentials.
38+
var ErrMissingCredentials = errors.New("name of the Secret containing PC credentials must be set")
39+
40+
type Config struct {
41+
*options.GlobalOptions
42+
43+
defaultValuesTemplateConfigMapName string
44+
}
45+
46+
func (c *Config) AddFlags(prefix string, flags *pflag.FlagSet) {
47+
flags.StringVar(
48+
&c.defaultValuesTemplateConfigMapName,
49+
prefix+".default-values-template-configmap-name",
50+
"default-nutanix-ccm-helm-values-template",
51+
"default values ConfigMap name",
52+
)
53+
}
54+
55+
type provider struct {
56+
client ctrlclient.Client
57+
config *Config
58+
}
59+
60+
func New(
61+
c ctrlclient.Client,
62+
cfg *Config,
63+
) *provider {
64+
return &provider{
65+
client: c,
66+
config: cfg,
67+
}
68+
}
69+
70+
func (p *provider) Apply(
71+
ctx context.Context,
72+
cluster *clusterv1.Cluster,
73+
clusterConfig *v1alpha1.ClusterConfigSpec,
74+
) error {
75+
// No need to check for nil values in the struct, this function will only be called if CCM is not nil
76+
if clusterConfig.Addons.CCM.Credentials == nil {
77+
return ErrMissingCredentials
78+
}
79+
80+
valuesTemplateConfigMap, err := lifecycleutils.RetrieveValuesTemplateConfigMap(
81+
ctx,
82+
p.client,
83+
p.config.defaultValuesTemplateConfigMapName,
84+
p.config.DefaultsNamespace(),
85+
)
86+
if err != nil {
87+
return fmt.Errorf(
88+
"failed to retrieve Nutanix CCM installation values template ConfigMap for cluster: %w",
89+
err,
90+
)
91+
}
92+
93+
// It's possible to have the credentials Secret be created by the Helm chart.
94+
// However, that would leave the credentials visible in the HelmChartProxy.
95+
// Instead, we'll create the Secret on the remote cluster and reference it in the Helm values.
96+
if clusterConfig.Addons.CCM.Credentials != nil {
97+
key := ctrlclient.ObjectKey{
98+
Name: defaultCredentialsSecretName,
99+
Namespace: defaultHelmReleaseNamespace,
100+
}
101+
err = lifecycleutils.CopySecretToRemoteCluster(
102+
ctx,
103+
p.client,
104+
clusterConfig.Addons.CCM.Credentials.Name,
105+
key,
106+
cluster,
107+
)
108+
if err != nil {
109+
return fmt.Errorf("error creating Nutanix CCM Credentials Secret on the remote cluster: %w", err)
110+
}
111+
}
112+
113+
values := valuesTemplateConfigMap.Data["values.yaml"]
114+
// The configMap will contain the Helm values, but templated with fields that need to be filled in.
115+
values, err = templateValues(clusterConfig, values)
116+
if err != nil {
117+
return fmt.Errorf("failed to template Helm values read from ConfigMap: %w", err)
118+
}
119+
120+
hcp := &caaphv1.HelmChartProxy{
121+
TypeMeta: metav1.TypeMeta{
122+
APIVersion: caaphv1.GroupVersion.String(),
123+
Kind: "HelmChartProxy",
124+
},
125+
ObjectMeta: metav1.ObjectMeta{
126+
Namespace: cluster.Namespace,
127+
Name: "nutanix-ccm-" + cluster.Name,
128+
},
129+
Spec: caaphv1.HelmChartProxySpec{
130+
RepoURL: defaultHelmRepositoryURL,
131+
ChartName: defaultHelmChartName,
132+
ClusterSelector: metav1.LabelSelector{
133+
MatchLabels: map[string]string{clusterv1.ClusterNameLabel: cluster.Name},
134+
},
135+
ReleaseNamespace: defaultHelmReleaseNamespace,
136+
ReleaseName: defaultHelmReleaseName,
137+
Version: defaultHelmChartVersion,
138+
ValuesTemplate: values,
139+
},
140+
}
141+
142+
if err = controllerutil.SetOwnerReference(cluster, hcp, p.client.Scheme()); err != nil {
143+
return fmt.Errorf(
144+
"failed to set owner reference on nutanix-ccm installation HelmChartProxy: %w",
145+
err,
146+
)
147+
}
148+
149+
if err = client.ServerSideApply(ctx, p.client, hcp); err != nil {
150+
return fmt.Errorf("failed to apply nutanix-ccm installation HelmChartProxy: %w", err)
151+
}
152+
153+
return nil
154+
}
155+
156+
func templateValues(clusterConfig *v1alpha1.ClusterConfigSpec, text string) (string, error) {
157+
helmValuesTemplate, err := template.New("").Parse(text)
158+
if err != nil {
159+
return "", fmt.Errorf("failed to parse Helm values template: %w", err)
160+
}
161+
162+
type input struct {
163+
PrismCentralEndpoint v1alpha1.NutanixPrismCentralEndpointSpec
164+
}
165+
templateInput := input{
166+
PrismCentralEndpoint: clusterConfig.Nutanix.PrismCentralEndpoint,
167+
}
168+
169+
var b bytes.Buffer
170+
err = helmValuesTemplate.Execute(&b, templateInput)
171+
if err != nil {
172+
return "", fmt.Errorf("failed setting PrismCentral configuration in template: %w", err)
173+
}
174+
175+
return b.String(), nil
176+
}

0 commit comments

Comments
 (0)