Skip to content

Commit ec74311

Browse files
Support user-supplied bootstrap configuration (#66)
1 parent 08084e8 commit ec74311

9 files changed

+247
-6
lines changed

bootstrap/api/v1beta2/ck8sconfig_types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type CK8sConfigSpec struct {
3333
// +optional
3434
Files []File `json:"files,omitempty"`
3535

36+
// BootstrapConfig is the data to be passed to the bootstrap script.
37+
BootstrapConfig *BootstrapConfig `json:"bootstrapConfig,omitempty"`
38+
3639
// BootCommands specifies extra commands to run in cloud-init early in the boot process.
3740
// +optional
3841
BootCommands []string `json:"bootCommands,omitempty"`
@@ -281,6 +284,17 @@ const (
281284
GzipBase64 Encoding = "gzip+base64"
282285
)
283286

287+
type BootstrapConfig struct {
288+
// Content is the actual content of the file.
289+
// If this is set, ContentFrom is ignored.
290+
// +optional
291+
Content string `json:"content,omitempty"`
292+
293+
// ContentFrom is a referenced source of content to populate the file.
294+
// +optional
295+
ContentFrom *FileSource `json:"contentFrom,omitempty"`
296+
}
297+
284298
// File defines the input for generating write_files in cloud-init.
285299
type File struct {
286300
// Path specifies the full path on disk where to store the file.

bootstrap/api/v1beta2/zz_generated.deepcopy.go

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

bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_ck8sconfigs.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,39 @@ spec:
5454
channel:
5555
description: Channel is the channel to use for the snap install.
5656
type: string
57+
bootstrapConfig:
58+
description: BootstrapConfig is the data to be passed to the bootstrap
59+
script.
60+
properties:
61+
content:
62+
description: |-
63+
Content is the actual content of the file.
64+
If this is set, ContentFrom is ignored.
65+
type: string
66+
contentFrom:
67+
description: ContentFrom is a referenced source of content to
68+
populate the file.
69+
properties:
70+
secret:
71+
description: Secret represents a secret that should populate
72+
this file.
73+
properties:
74+
key:
75+
description: Key is the key in the secret's data map for
76+
this value.
77+
type: string
78+
name:
79+
description: Name of the secret in the CK8sBootstrapConfig's
80+
namespace to use.
81+
type: string
82+
required:
83+
- key
84+
- name
85+
type: object
86+
required:
87+
- secret
88+
type: object
89+
type: object
5790
controlPlane:
5891
description: CK8sControlPlaneConfig is configuration for the control
5992
plane node.

bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_ck8sconfigtemplates.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,39 @@ spec:
6161
channel:
6262
description: Channel is the channel to use for the snap install.
6363
type: string
64+
bootstrapConfig:
65+
description: BootstrapConfig is the data to be passed to the
66+
bootstrap script.
67+
properties:
68+
content:
69+
description: |-
70+
Content is the actual content of the file.
71+
If this is set, ContentFrom is ignored.
72+
type: string
73+
contentFrom:
74+
description: ContentFrom is a referenced source of content
75+
to populate the file.
76+
properties:
77+
secret:
78+
description: Secret represents a secret that should
79+
populate this file.
80+
properties:
81+
key:
82+
description: Key is the key in the secret's data
83+
map for this value.
84+
type: string
85+
name:
86+
description: Name of the secret in the CK8sBootstrapConfig's
87+
namespace to use.
88+
type: string
89+
required:
90+
- key
91+
- name
92+
type: object
93+
required:
94+
- secret
95+
type: object
96+
type: object
6497
controlPlane:
6598
description: CK8sControlPlaneConfig is configuration for the
6699
control plane node.

bootstrap/controllers/ck8sconfig_controller.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,32 @@ func (r *CK8sConfigReconciler) joinWorker(ctx context.Context, scope *Scope) err
404404
return nil
405405
}
406406

407+
// resolveUserBootstrapConfig returns the bootstrap configuration provided by the user.
408+
// It can resolve string content, a reference to a secret, or an empty string if no configuration was provided.
409+
func (r *CK8sConfigReconciler) resolveUserBootstrapConfig(ctx context.Context, cfg *bootstrapv1.CK8sConfig) (string, error) {
410+
// User did not provide a bootstrap configuration
411+
if cfg.Spec.BootstrapConfig == nil {
412+
return "", nil
413+
}
414+
415+
// User provided a bootstrap configuration through content
416+
if cfg.Spec.BootstrapConfig.Content != "" {
417+
return cfg.Spec.BootstrapConfig.Content, nil
418+
}
419+
420+
// User referenced a secret for the bootstrap configuration
421+
if cfg.Spec.BootstrapConfig.ContentFrom == nil {
422+
return "", nil
423+
}
424+
425+
data, err := r.resolveSecretFileContent(ctx, cfg.Namespace, *cfg.Spec.BootstrapConfig.ContentFrom)
426+
if err != nil {
427+
return "", fmt.Errorf("failed to read bootstrap configuration from secret %q: %w", cfg.Spec.BootstrapConfig.ContentFrom.Secret.Name, err)
428+
}
429+
430+
return string(data), nil
431+
}
432+
407433
// resolveFiles maps .Spec.Files into cloudinit.Files, resolving any object references
408434
// along the way.
409435
func (r *CK8sConfigReconciler) resolveFiles(ctx context.Context, cfg *bootstrapv1.CK8sConfig) ([]bootstrapv1.File, error) {
@@ -412,7 +438,7 @@ func (r *CK8sConfigReconciler) resolveFiles(ctx context.Context, cfg *bootstrapv
412438
for i := range cfg.Spec.Files {
413439
in := cfg.Spec.Files[i]
414440
if in.ContentFrom != nil {
415-
data, err := r.resolveSecretFileContent(ctx, cfg.Namespace, in)
441+
data, err := r.resolveSecretFileContent(ctx, cfg.Namespace, *in.ContentFrom)
416442
if err != nil {
417443
return nil, fmt.Errorf("failed to resolve file source: %w", err)
418444
}
@@ -505,18 +531,18 @@ func (r *CK8sConfigReconciler) getSnapInstallDataFromSpec(spec bootstrapv1.CK8sC
505531
}
506532

507533
// resolveSecretFileContent returns file content fetched from a referenced secret object.
508-
func (r *CK8sConfigReconciler) resolveSecretFileContent(ctx context.Context, ns string, source bootstrapv1.File) ([]byte, error) {
534+
func (r *CK8sConfigReconciler) resolveSecretFileContent(ctx context.Context, ns string, source bootstrapv1.FileSource) ([]byte, error) {
509535
secret := &corev1.Secret{}
510-
key := types.NamespacedName{Namespace: ns, Name: source.ContentFrom.Secret.Name}
536+
key := types.NamespacedName{Namespace: ns, Name: source.Secret.Name}
511537
if err := r.Client.Get(ctx, key, secret); err != nil {
512538
if apierrors.IsNotFound(err) {
513539
return nil, fmt.Errorf("secret not found %s: %w", key, err)
514540
}
515541
return nil, fmt.Errorf("failed to retrieve Secret %q: %w", key, err)
516542
}
517-
data, ok := secret.Data[source.ContentFrom.Secret.Key]
543+
data, ok := secret.Data[source.Secret.Key]
518544
if !ok {
519-
return nil, fmt.Errorf("secret references non-existent secret key %q: %w", source.ContentFrom.Secret.Key, ErrInvalidRef)
545+
return nil, fmt.Errorf("secret references non-existent secret key %q: %w", source.Secret.Key, ErrInvalidRef)
520546
}
521547
return data, nil
522548
}
@@ -636,6 +662,12 @@ func (r *CK8sConfigReconciler) handleClusterNotInitialized(ctx context.Context,
636662
return ctrl.Result{}, err
637663
}
638664

665+
userSuppliedBootstrapConfig, err := r.resolveUserBootstrapConfig(ctx, scope.Config)
666+
if err != nil {
667+
conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
668+
return ctrl.Result{}, err
669+
}
670+
639671
microclusterPort := scope.Config.Spec.ControlPlaneConfig.GetMicroclusterPort()
640672
ds, err := ck8s.RenderK8sdProxyDaemonSetManifest(ck8s.K8sdProxyDaemonSetInput{K8sdPort: microclusterPort})
641673
if err != nil {
@@ -654,6 +686,7 @@ func (r *CK8sConfigReconciler) handleClusterNotInitialized(ctx context.Context,
654686
PreRunCommands: scope.Config.Spec.PreRunCommands,
655687
PostRunCommands: scope.Config.Spec.PostRunCommands,
656688
KubernetesVersion: scope.Config.Spec.Version,
689+
BootstrapConfig: userSuppliedBootstrapConfig,
657690
SnapInstallData: snapInstallData,
658691
ExtraFiles: cloudinit.FilesFromAPI(files),
659692
ConfigFileContents: string(initConfig),

controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_ck8scontrolplanes.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,39 @@ spec:
249249
channel:
250250
description: Channel is the channel to use for the snap install.
251251
type: string
252+
bootstrapConfig:
253+
description: BootstrapConfig is the data to be passed to the bootstrap
254+
script.
255+
properties:
256+
content:
257+
description: |-
258+
Content is the actual content of the file.
259+
If this is set, ContentFrom is ignored.
260+
type: string
261+
contentFrom:
262+
description: ContentFrom is a referenced source of content
263+
to populate the file.
264+
properties:
265+
secret:
266+
description: Secret represents a secret that should populate
267+
this file.
268+
properties:
269+
key:
270+
description: Key is the key in the secret's data map
271+
for this value.
272+
type: string
273+
name:
274+
description: Name of the secret in the CK8sBootstrapConfig's
275+
namespace to use.
276+
type: string
277+
required:
278+
- key
279+
- name
280+
type: object
281+
required:
282+
- secret
283+
type: object
284+
type: object
252285
controlPlane:
253286
description: CK8sControlPlaneConfig is configuration for the control
254287
plane node.

controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_ck8scontrolplanetemplates.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,39 @@ spec:
225225
description: Channel is the channel to use for the snap
226226
install.
227227
type: string
228+
bootstrapConfig:
229+
description: BootstrapConfig is the data to be passed
230+
to the bootstrap script.
231+
properties:
232+
content:
233+
description: |-
234+
Content is the actual content of the file.
235+
If this is set, ContentFrom is ignored.
236+
type: string
237+
contentFrom:
238+
description: ContentFrom is a referenced source of
239+
content to populate the file.
240+
properties:
241+
secret:
242+
description: Secret represents a secret that should
243+
populate this file.
244+
properties:
245+
key:
246+
description: Key is the key in the secret's
247+
data map for this value.
248+
type: string
249+
name:
250+
description: Name of the secret in the CK8sBootstrapConfig's
251+
namespace to use.
252+
type: string
253+
required:
254+
- key
255+
- name
256+
type: object
257+
required:
258+
- secret
259+
type: object
260+
type: object
228261
controlPlane:
229262
description: CK8sControlPlaneConfig is configuration for
230263
the control plane node.

pkg/cloudinit/common.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type BaseUserData struct {
3535
PreRunCommands []string
3636
// PostRunCommands is a list of commands to run after k8s installation.
3737
PostRunCommands []string
38+
// BootstrapConfig is the contents of the bootstrap configuration file.
39+
BootstrapConfig string
3840
// ExtraFiles is a list of extra files to load on the host.
3941
ExtraFiles []File
4042
// ConfigFileContents is the contents of the k8s configuration file.
@@ -93,14 +95,21 @@ func NewBaseCloudConfig(data BaseUserData) (CloudConfig, error) {
9395
config.RunCommands = append(config.RunCommands, "/capi/scripts/configure-snapstore-proxy.sh")
9496
}
9597

98+
var configFileContents string
99+
if data.BootstrapConfig != "" {
100+
configFileContents = data.BootstrapConfig
101+
} else {
102+
configFileContents = data.ConfigFileContents
103+
}
104+
96105
// write files
97106
config.WriteFiles = append(
98107
config.WriteFiles,
99108
append(
100109
data.ExtraFiles,
101110
File{
102111
Path: "/capi/etc/config.yaml",
103-
Content: data.ConfigFileContents,
112+
Content: configFileContents,
104113
Permissions: "0400",
105114
Owner: "root:root",
106115
},

pkg/cloudinit/controlplane_init_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222

2323
. "github.com/onsi/gomega"
24+
format "github.com/onsi/gomega/format"
2425
"github.com/onsi/gomega/gstruct"
2526

2627
"github.com/canonical/cluster-api-k8s/pkg/cloudinit"
@@ -29,6 +30,8 @@ import (
2930
func TestNewInitControlPlane(t *testing.T) {
3031
g := NewWithT(t)
3132

33+
format.MaxLength = 20000
34+
3235
config, err := cloudinit.NewInitControlPlane(cloudinit.InitControlPlaneInput{
3336
BaseUserData: cloudinit.BaseUserData{
3437
KubernetesVersion: "v1.30.0",
@@ -102,6 +105,31 @@ func TestNewInitControlPlane(t *testing.T) {
102105
), "Some /capi/scripts files are missing")
103106
}
104107

108+
func TestUserSuppliedBootstrapConfig(t *testing.T) {
109+
g := NewWithT(t)
110+
111+
config, err := cloudinit.NewInitControlPlane(cloudinit.InitControlPlaneInput{
112+
BaseUserData: cloudinit.BaseUserData{
113+
KubernetesVersion: "v1.30.0",
114+
BootstrapConfig: "### bootstrap config ###",
115+
ConfigFileContents: "### config file ###",
116+
},
117+
})
118+
119+
g.Expect(err).ToNot(HaveOccurred())
120+
121+
// Test that user-supplied bootstrap configuration takes precedence over ConfigFileContents.
122+
g.Expect(config.WriteFiles).To(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
123+
"Path": Equal("/capi/etc/config.yaml"),
124+
"Content": Equal("### bootstrap config ###"),
125+
})))
126+
127+
g.Expect(config.WriteFiles).NotTo(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
128+
"Path": Equal("/capi/etc/config.yaml"),
129+
"Content": Equal("### config file ###"),
130+
})))
131+
}
132+
105133
func TestNewInitControlPlaneInvalidVersionError(t *testing.T) {
106134
g := NewWithT(t)
107135

0 commit comments

Comments
 (0)