Skip to content

Commit 851c053

Browse files
committed
initial implementation
1 parent 9d55458 commit 851c053

10 files changed

+257
-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"`
@@ -252,6 +255,17 @@ const (
252255
GzipBase64 Encoding = "gzip+base64"
253256
)
254257

258+
type BootstrapConfig struct {
259+
// Content is the actual content of the file.
260+
// If this is set, ContentFrom is ignored.
261+
// +optional
262+
Content string `json:"content,omitempty"`
263+
264+
// ContentFrom is a referenced source of content to populate the file.
265+
// +optional
266+
ContentFrom *FileSource `json:"contentFrom,omitempty"`
267+
}
268+
255269
// File defines the input for generating write_files in cloud-init.
256270
type File struct {
257271
// 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
@@ -51,6 +51,39 @@ spec:
5151
items:
5252
type: string
5353
type: array
54+
bootstrapConfig:
55+
description: BootstrapConfig is the data to be passed to the bootstrap
56+
script.
57+
properties:
58+
content:
59+
description: |-
60+
Content is the actual content of the file.
61+
If this is set, ContentFrom is ignored.
62+
type: string
63+
contentFrom:
64+
description: ContentFrom is a referenced source of content to
65+
populate the file.
66+
properties:
67+
secret:
68+
description: Secret represents a secret that should populate
69+
this file.
70+
properties:
71+
key:
72+
description: Key is the key in the secret's data map for
73+
this value.
74+
type: string
75+
name:
76+
description: Name of the secret in the CK8sBootstrapConfig's
77+
namespace to use.
78+
type: string
79+
required:
80+
- key
81+
- name
82+
type: object
83+
required:
84+
- secret
85+
type: object
86+
type: object
5487
controlPlane:
5588
description: CK8sControlPlaneConfig is configuration for the control
5689
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
@@ -58,6 +58,39 @@ spec:
5858
items:
5959
type: string
6060
type: array
61+
bootstrapConfig:
62+
description: BootstrapConfig is the data to be passed to the
63+
bootstrap script.
64+
properties:
65+
content:
66+
description: |-
67+
Content is the actual content of the file.
68+
If this is set, ContentFrom is ignored.
69+
type: string
70+
contentFrom:
71+
description: ContentFrom is a referenced source of content
72+
to populate the file.
73+
properties:
74+
secret:
75+
description: Secret represents a secret that should
76+
populate this file.
77+
properties:
78+
key:
79+
description: Key is the key in the secret's data
80+
map for this value.
81+
type: string
82+
name:
83+
description: Name of the secret in the CK8sBootstrapConfig's
84+
namespace to use.
85+
type: string
86+
required:
87+
- key
88+
- name
89+
type: object
90+
required:
91+
- secret
92+
type: object
93+
type: object
6194
controlPlane:
6295
description: CK8sControlPlaneConfig is configuration for the
6396
control plane node.

bootstrap/controllers/ck8sconfig_controller.go

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

379+
// resolveUserBootstrapConfig returns the bootstrap configuration provided by the user.
380+
// It can resolve string content, a reference to a secret, or an empty string if no configuration was provided.
381+
func (r *CK8sConfigReconciler) resolveUserBootstrapConfig(ctx context.Context, cfg *bootstrapv1.CK8sConfig) (string, error) {
382+
// User did not provide a bootstrap configuration
383+
if cfg.Spec.BootstrapConfig == nil {
384+
return "", nil
385+
}
386+
387+
// User provided a bootstrap configuration through content
388+
if cfg.Spec.BootstrapConfig.Content != "" {
389+
return cfg.Spec.BootstrapConfig.Content, nil
390+
}
391+
392+
// User referenced a secret for the bootstrap configuration
393+
if cfg.Spec.BootstrapConfig.ContentFrom == nil {
394+
return "", nil
395+
}
396+
397+
data, err := r.resolveSecretFileContent(ctx, cfg.Namespace, *cfg.Spec.BootstrapConfig.ContentFrom)
398+
if err != nil {
399+
return "", fmt.Errorf("failed to read bootstrap configuration from secret: %w", err)
400+
}
401+
402+
return string(data), nil
403+
}
404+
379405
// resolveFiles maps .Spec.Files into cloudinit.Files, resolving any object references
380406
// along the way.
381407
func (r *CK8sConfigReconciler) resolveFiles(ctx context.Context, cfg *bootstrapv1.CK8sConfig) ([]bootstrapv1.File, error) {
@@ -384,7 +410,7 @@ func (r *CK8sConfigReconciler) resolveFiles(ctx context.Context, cfg *bootstrapv
384410
for i := range cfg.Spec.Files {
385411
in := cfg.Spec.Files[i]
386412
if in.ContentFrom != nil {
387-
data, err := r.resolveSecretFileContent(ctx, cfg.Namespace, in)
413+
data, err := r.resolveSecretFileContent(ctx, cfg.Namespace, *in.ContentFrom)
388414
if err != nil {
389415
return nil, fmt.Errorf("failed to resolve file source: %w", err)
390416
}
@@ -433,18 +459,18 @@ func (r *CK8sConfigReconciler) resolveInPlaceUpgradeRelease(machine *clusterv1.M
433459
}
434460

435461
// resolveSecretFileContent returns file content fetched from a referenced secret object.
436-
func (r *CK8sConfigReconciler) resolveSecretFileContent(ctx context.Context, ns string, source bootstrapv1.File) ([]byte, error) {
462+
func (r *CK8sConfigReconciler) resolveSecretFileContent(ctx context.Context, ns string, source bootstrapv1.FileSource) ([]byte, error) {
437463
secret := &corev1.Secret{}
438-
key := types.NamespacedName{Namespace: ns, Name: source.ContentFrom.Secret.Name}
464+
key := types.NamespacedName{Namespace: ns, Name: source.Secret.Name}
439465
if err := r.Client.Get(ctx, key, secret); err != nil {
440466
if apierrors.IsNotFound(err) {
441467
return nil, fmt.Errorf("secret not found %s: %w", key, err)
442468
}
443469
return nil, fmt.Errorf("failed to retrieve Secret %q: %w", key, err)
444470
}
445-
data, ok := secret.Data[source.ContentFrom.Secret.Key]
471+
data, ok := secret.Data[source.Secret.Key]
446472
if !ok {
447-
return nil, fmt.Errorf("secret references non-existent secret key %q: %w", source.ContentFrom.Secret.Key, ErrInvalidRef)
473+
return nil, fmt.Errorf("secret references non-existent secret key %q: %w", source.Secret.Key, ErrInvalidRef)
448474
}
449475
return data, nil
450476
}
@@ -564,6 +590,12 @@ func (r *CK8sConfigReconciler) handleClusterNotInitialized(ctx context.Context,
564590
return ctrl.Result{}, err
565591
}
566592

593+
userSuppliedBootstrapConfig, err := r.resolveUserBootstrapConfig(ctx, scope.Config)
594+
if err != nil {
595+
conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
596+
return ctrl.Result{}, err
597+
}
598+
567599
microclusterPort := scope.Config.Spec.ControlPlaneConfig.GetMicroclusterPort()
568600
ds, err := ck8s.RenderK8sdProxyDaemonSetManifest(ck8s.K8sdProxyDaemonSetInput{K8sdPort: microclusterPort})
569601
if err != nil {
@@ -576,6 +608,7 @@ func (r *CK8sConfigReconciler) handleClusterNotInitialized(ctx context.Context,
576608
PreRunCommands: scope.Config.Spec.PreRunCommands,
577609
PostRunCommands: scope.Config.Spec.PostRunCommands,
578610
KubernetesVersion: scope.Config.Spec.Version,
611+
BootstrapConfig: userSuppliedBootstrapConfig,
579612
ExtraFiles: cloudinit.FilesFromAPI(files),
580613
ConfigFileContents: string(initConfig),
581614
MicroclusterAddress: scope.Config.Spec.ControlPlaneConfig.MicroclusterAddress,

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
@@ -246,6 +246,39 @@ spec:
246246
items:
247247
type: string
248248
type: array
249+
bootstrapConfig:
250+
description: BootstrapConfig is the data to be passed to the bootstrap
251+
script.
252+
properties:
253+
content:
254+
description: |-
255+
Content is the actual content of the file.
256+
If this is set, ContentFrom is ignored.
257+
type: string
258+
contentFrom:
259+
description: ContentFrom is a referenced source of content
260+
to populate the file.
261+
properties:
262+
secret:
263+
description: Secret represents a secret that should populate
264+
this file.
265+
properties:
266+
key:
267+
description: Key is the key in the secret's data map
268+
for this value.
269+
type: string
270+
name:
271+
description: Name of the secret in the CK8sBootstrapConfig's
272+
namespace to use.
273+
type: string
274+
required:
275+
- key
276+
- name
277+
type: object
278+
required:
279+
- secret
280+
type: object
281+
type: object
249282
controlPlane:
250283
description: CK8sControlPlaneConfig is configuration for the control
251284
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
@@ -221,6 +221,39 @@ spec:
221221
items:
222222
type: string
223223
type: array
224+
bootstrapConfig:
225+
description: BootstrapConfig is the data to be passed
226+
to the bootstrap script.
227+
properties:
228+
content:
229+
description: |-
230+
Content is the actual content of the file.
231+
If this is set, ContentFrom is ignored.
232+
type: string
233+
contentFrom:
234+
description: ContentFrom is a referenced source of
235+
content to populate the file.
236+
properties:
237+
secret:
238+
description: Secret represents a secret that should
239+
populate this file.
240+
properties:
241+
key:
242+
description: Key is the key in the secret's
243+
data map for this value.
244+
type: string
245+
name:
246+
description: Name of the secret in the CK8sBootstrapConfig's
247+
namespace to use.
248+
type: string
249+
required:
250+
- key
251+
- name
252+
type: object
253+
required:
254+
- secret
255+
type: object
256+
type: object
224257
controlPlane:
225258
description: CK8sControlPlaneConfig is configuration for
226259
the control plane node.

pkg/cloudinit/common.go

Lines changed: 13 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 boop bop
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.
@@ -78,14 +80,24 @@ func NewBaseCloudConfig(data BaseUserData) (CloudConfig, error) {
7880
Owner: "root:root",
7981
})
8082
}
83+
84+
var configFileContents string
85+
switch {
86+
case data.BootstrapConfig != "":
87+
// User-supplied bootstrap configuration from CK8sConfig object.
88+
configFileContents = data.BootstrapConfig
89+
default:
90+
configFileContents = data.ConfigFileContents
91+
}
92+
8193
// write files
8294
config.WriteFiles = append(
8395
config.WriteFiles,
8496
append(
8597
data.ExtraFiles,
8698
File{
8799
Path: "/capi/etc/config.yaml",
88-
Content: data.ConfigFileContents,
100+
Content: configFileContents,
89101
Permissions: "0400",
90102
Owner: "root:root",
91103
},

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",
@@ -92,6 +95,31 @@ func TestNewInitControlPlane(t *testing.T) {
9295
), "Some /capi/scripts files are missing")
9396
}
9497

98+
func TestUserSuppliedBootstrapConfig(t *testing.T) {
99+
g := NewWithT(t)
100+
101+
config, err := cloudinit.NewInitControlPlane(cloudinit.InitControlPlaneInput{
102+
BaseUserData: cloudinit.BaseUserData{
103+
KubernetesVersion: "v1.30.0",
104+
BootstrapConfig: "### bootstrap config ###",
105+
ConfigFileContents: "### config file ###",
106+
},
107+
})
108+
109+
g.Expect(err).ToNot(HaveOccurred())
110+
111+
// Test that user-supplied bootstrap configuration takes precedence over ConfigFileContents.
112+
g.Expect(config.WriteFiles).To(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
113+
"Path": Equal("/capi/etc/config.yaml"),
114+
"Content": Equal("### bootstrap config ###"),
115+
})))
116+
117+
g.Expect(config.WriteFiles).NotTo(ContainElement(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
118+
"Path": Equal("/capi/etc/config.yaml"),
119+
"Content": Equal("### config file ###"),
120+
})))
121+
}
122+
95123
func TestNewInitControlPlaneInvalidVersionError(t *testing.T) {
96124
g := NewWithT(t)
97125

0 commit comments

Comments
 (0)