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

Commit ee58bbd

Browse files
committed
feat: deploy Nutanix CCM addon
Aligns the method of deploying the CCM with all other addons.
1 parent 43ff36c commit ee58bbd

File tree

12 files changed

+417
-34
lines changed

12 files changed

+417
-34
lines changed

api/v1alpha1/addon_types.go

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

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

313317
func (CCM) VariableSchema() clusterv1.VariableSchema {
318+
// TODO Validate credentials is set.
319+
// This CCM is shared across all providers.
320+
// Some of these providers may require credentials to be set, but we don't want to require it for all providers.
321+
// The Nutanix CCM handler will fail in at runtime if credentials are not set.
314322
return clusterv1.VariableSchema{
315323
OpenAPIV3Schema: clusterv1.JSONSchemaProps{
316324
Type: "object",
325+
Properties: map[string]clusterv1.JSONSchemaProps{
326+
"credentials": {
327+
Description: "A reference to the Secret for credential information" +
328+
"for the target Prism Central instance",
329+
Type: "object",
330+
Properties: map[string]clusterv1.JSONSchemaProps{
331+
"name": {
332+
Description: "The name of the Secret",
333+
Type: "string",
334+
},
335+
},
336+
Required: []string{"name"},
337+
},
338+
},
317339
},
318340
}
319341
}

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/nutanix_clusterconfig_types.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
package v1alpha1
55

66
import (
7+
"fmt"
8+
"net/url"
9+
"strconv"
10+
711
corev1 "k8s.io/api/core/v1"
812
"k8s.io/utils/ptr"
913
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -99,3 +103,26 @@ func (NutanixPrismCentralEndpointSpec) VariableSchema() clusterv1.VariableSchema
99103
},
100104
}
101105
}
106+
107+
//nolint:gocritic // no need for named return values
108+
func (s NutanixPrismCentralEndpointSpec) ParseURL() (string, int32, error) {
109+
var prismCentralURL *url.URL
110+
prismCentralURL, err := url.Parse(s.URL)
111+
if err != nil {
112+
return "", -1, fmt.Errorf("error parsing Prism Central URL: %w", err)
113+
}
114+
115+
hostname := prismCentralURL.Hostname()
116+
117+
// return early with the default port if no port is specified
118+
if prismCentralURL.Port() == "" {
119+
return hostname, DefaultPrismCentralPort, nil
120+
}
121+
122+
port, err := strconv.ParseInt(prismCentralURL.Port(), 10, 32)
123+
if err != nil {
124+
return "", -1, fmt.Errorf("error converting port to int: %w", err)
125+
}
126+
127+
return hostname, int32(port), nil
128+
}

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: 25 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,39 @@ func (c *CCMHandler) AfterControlPlaneInitialized(
8888
log.V(4).Info("Skipping CCM handler.")
8989
return
9090
}
91+
92+
clusterConfigVar, _, err := variables.Get[v1alpha1.ClusterConfigSpec](
93+
varMap,
94+
clusterconfig.MetaVariableName,
95+
)
96+
if err != nil {
97+
log.Error(
98+
err,
99+
"failed to read clusterConfig variable from cluster definition",
100+
)
101+
resp.SetStatus(runtimehooksv1.ResponseStatusFailure)
102+
resp.SetMessage(
103+
fmt.Sprintf("ffailed to read clusterConfig variable from cluster definition: %v",
104+
err,
105+
),
106+
)
107+
return
108+
}
109+
91110
infraKind := req.Cluster.Spec.InfrastructureRef.Kind
92111
log.Info(fmt.Sprintf("finding CCM handler for %s", infraKind))
93112
var handler CCMProvider
94113
switch {
95114
case strings.Contains(strings.ToLower(infraKind), v1alpha1.CCMProviderAWS):
96115
handler = c.ProviderHandler[v1alpha1.CCMProviderAWS]
116+
case strings.Contains(strings.ToLower(infraKind), v1alpha1.CCMProviderNutanix):
117+
handler = c.ProviderHandler[v1alpha1.CCMProviderNutanix]
97118
default:
98119
log.Info(fmt.Sprintf("No CCM handler provided for infra kind %s", infraKind))
99120
return
100121
}
101-
err = handler.Apply(ctx, &req.Cluster)
122+
123+
err = handler.Apply(ctx, &req.Cluster, &clusterConfigVar)
102124
if err != nil {
103125
log.Error(
104126
err,
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
ctrl "sigs.k8s.io/controller-runtime"
17+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
18+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
19+
20+
caaphv1 "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/external/sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1"
21+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
22+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/k8s/client"
23+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/config"
24+
lifecycleutils "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/utils"
25+
"github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
26+
)
27+
28+
const (
29+
defaultHelmReleaseName = "nutanix-ccm"
30+
defaultHelmReleaseNamespace = "kube-system"
31+
32+
// This is the name of the Secret on the remote cluster that should match what is defined in Helm values.
33+
//nolint:gosec // Does not contain hard coded credentials.
34+
defaultCredentialsSecretName = "nutanix-ccm-credentials"
35+
)
36+
37+
var ErrMissingCredentials = errors.New("name of the Secret containing PC credentials must be set")
38+
39+
type Config struct {
40+
*options.GlobalOptions
41+
42+
defaultValuesTemplateConfigMapName string
43+
}
44+
45+
func (c *Config) AddFlags(prefix string, flags *pflag.FlagSet) {
46+
flags.StringVar(
47+
&c.defaultValuesTemplateConfigMapName,
48+
prefix+".default-values-template-configmap-name",
49+
"default-nutanix-ccm-helm-values-template",
50+
"default values ConfigMap name",
51+
)
52+
}
53+
54+
type provider struct {
55+
client ctrlclient.Client
56+
config *Config
57+
helmChartInfoGetter *config.HelmChartGetter
58+
}
59+
60+
func New(
61+
c ctrlclient.Client,
62+
cfg *Config,
63+
helmChartInfoGetter *config.HelmChartGetter,
64+
) *provider {
65+
return &provider{
66+
client: c,
67+
config: cfg,
68+
helmChartInfoGetter: helmChartInfoGetter,
69+
}
70+
}
71+
72+
func (p *provider) Apply(
73+
ctx context.Context,
74+
cluster *clusterv1.Cluster,
75+
clusterConfig *v1alpha1.ClusterConfigSpec,
76+
) error {
77+
// No need to check for nil values in the struct, this function will only be called if CCM is not nil
78+
if clusterConfig.Addons.CCM.Credentials == nil {
79+
return ErrMissingCredentials
80+
}
81+
82+
valuesTemplateConfigMap, err := lifecycleutils.RetrieveValuesTemplateConfigMap(
83+
ctx,
84+
p.client,
85+
p.config.defaultValuesTemplateConfigMapName,
86+
p.config.DefaultsNamespace(),
87+
)
88+
if err != nil {
89+
return fmt.Errorf(
90+
"failed to retrieve Nutanix CCM installation values template ConfigMap for cluster: %w",
91+
err,
92+
)
93+
}
94+
95+
// It's possible to have the credentials Secret be created by the Helm chart.
96+
// However, that would leave the credentials visible in the HelmChartProxy.
97+
// Instead, we'll create the Secret on the remote cluster and reference it in the Helm values.
98+
if clusterConfig.Addons.CCM.Credentials != nil {
99+
key := ctrlclient.ObjectKey{
100+
Name: defaultCredentialsSecretName,
101+
Namespace: defaultHelmReleaseNamespace,
102+
}
103+
err = lifecycleutils.CopySecretToRemoteCluster(
104+
ctx,
105+
p.client,
106+
clusterConfig.Addons.CCM.Credentials.Name,
107+
key,
108+
cluster,
109+
)
110+
if err != nil {
111+
return fmt.Errorf(
112+
"error creating Nutanix CCM Credentials Secret on the remote cluster: %w",
113+
err,
114+
)
115+
}
116+
}
117+
118+
log := ctrl.LoggerFrom(ctx).WithValues(
119+
"cluster",
120+
ctrlclient.ObjectKeyFromObject(cluster),
121+
)
122+
helmChart, err := p.helmChartInfoGetter.For(ctx, log, config.NutanixCCM)
123+
if err != nil {
124+
return fmt.Errorf("failed to get values for nutanix-ccm-config %w", err)
125+
}
126+
127+
values := valuesTemplateConfigMap.Data["values.yaml"]
128+
// The configMap will contain the Helm values, but templated with fields that need to be filled in.
129+
values, err = templateValues(clusterConfig, values)
130+
if err != nil {
131+
return fmt.Errorf("failed to template Helm values read from ConfigMap: %w", err)
132+
}
133+
134+
hcp := &caaphv1.HelmChartProxy{
135+
TypeMeta: metav1.TypeMeta{
136+
APIVersion: caaphv1.GroupVersion.String(),
137+
Kind: "HelmChartProxy",
138+
},
139+
ObjectMeta: metav1.ObjectMeta{
140+
Namespace: cluster.Namespace,
141+
Name: "nutanix-ccm-" + cluster.Name,
142+
},
143+
Spec: caaphv1.HelmChartProxySpec{
144+
RepoURL: helmChart.Repository,
145+
ChartName: helmChart.Name,
146+
ClusterSelector: metav1.LabelSelector{
147+
MatchLabels: map[string]string{clusterv1.ClusterNameLabel: cluster.Name},
148+
},
149+
ReleaseNamespace: defaultHelmReleaseNamespace,
150+
ReleaseName: defaultHelmReleaseName,
151+
Version: helmChart.Version,
152+
ValuesTemplate: values,
153+
},
154+
}
155+
156+
if err = controllerutil.SetOwnerReference(cluster, hcp, p.client.Scheme()); err != nil {
157+
return fmt.Errorf(
158+
"failed to set owner reference on nutanix-ccm installation HelmChartProxy: %w",
159+
err,
160+
)
161+
}
162+
163+
if err = client.ServerSideApply(ctx, p.client, hcp); err != nil {
164+
return fmt.Errorf("failed to apply nutanix-ccm installation HelmChartProxy: %w", err)
165+
}
166+
167+
return nil
168+
}
169+
170+
func templateValues(clusterConfig *v1alpha1.ClusterConfigSpec, text string) (string, error) {
171+
helmValuesTemplate, err := template.New("").Parse(text)
172+
if err != nil {
173+
return "", fmt.Errorf("failed to parse Helm values template: %w", err)
174+
}
175+
176+
type input struct {
177+
PrismCentralHost string
178+
PrismCentralPort int32
179+
PrismCentralInsecure bool
180+
PrismCentralAdditionalTrustBundle *string
181+
}
182+
183+
address, port, err := clusterConfig.Nutanix.PrismCentralEndpoint.ParseURL()
184+
if err != nil {
185+
return "", err
186+
}
187+
templateInput := input{
188+
PrismCentralHost: address,
189+
PrismCentralPort: port,
190+
PrismCentralInsecure: clusterConfig.Nutanix.PrismCentralEndpoint.Insecure,
191+
PrismCentralAdditionalTrustBundle: clusterConfig.Nutanix.PrismCentralEndpoint.AdditionalTrustBundle,
192+
}
193+
194+
var b bytes.Buffer
195+
err = helmValuesTemplate.Execute(&b, templateInput)
196+
if err != nil {
197+
return "", fmt.Errorf("failed setting PrismCentral configuration in template: %w", err)
198+
}
199+
200+
return b.String(), nil
201+
}

0 commit comments

Comments
 (0)