Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion api/bootstrap/kubeadm/v1beta1/kubeadmconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var (
missingSecretNameMsg = "secret file source must specify non-empty secret name"
missingSecretKeyMsg = "secret file source must specify non-empty secret key"
pathConflictMsg = "path property must be unique among all files"
invalidFileContentFormatMsg = "contentFormat must be empty or go-template"
)

// KubeadmConfigSpec defines the desired state of KubeadmConfig.
Expand Down Expand Up @@ -220,6 +221,16 @@ func (c *KubeadmConfigSpec) validateFiles(pathPrefix *field.Path) field.ErrorLis
),
)
}
if file.ContentFormat != "" && file.ContentFormat != FileContentFormatGoTemplate {
allErrs = append(
allErrs,
field.Invalid(
pathPrefix.Child("files").Index(i).Child("contentFormat"),
file.ContentFormat,
invalidFileContentFormatMsg,
),
)
}
// n.b.: if we ever add types besides Secret as a ContentFrom
// Source, we must add webhook validation here for one of the
// sources being non-nil.
Expand Down Expand Up @@ -497,7 +508,7 @@ type KubeadmConfigStatus struct {
// See https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more context.
type KubeadmConfigV1Beta2Status struct {
// conditions represents the observations of a KubeadmConfig's current state.
// Known condition types are Ready, DataSecretAvailable, CertificatesAvailable.
// Known condition types are Ready, DataSecretAvailable, CertificatesAvailable, ControlPlaneKubernetesVersionAvailable.
// +optional
// +listType=map
// +listMapKey=type
Expand Down Expand Up @@ -575,6 +586,16 @@ func init() {
// +kubebuilder:validation:Enum=base64;gzip;gzip+base64
type Encoding string

// FileContentFormat specifies how file content is interpreted after resolving content/contentFrom and before writing bootstrap data.
// +kubebuilder:validation:Enum="";go-template
type FileContentFormat string

const (
// FileContentFormatGoTemplate means content is rendered as a Go text/template with KubernetesVersion and other
// fields documented by the kubeadm bootstrap provider. The default empty value means content is used verbatim.
FileContentFormatGoTemplate FileContentFormat = "go-template"
)

const (
// Base64 implies the contents of the file are encoded as base64.
Base64 Encoding = "base64"
Expand Down Expand Up @@ -608,6 +629,13 @@ type File struct {
// +optional
Encoding Encoding `json:"encoding,omitempty"`

// contentFormat specifies how to interpret content after it is loaded (inline or from contentFrom).
// When set to "go-template", content is rendered as a Go text/template with data including KubernetesVersion
// (semver.String(), no "v" prefix). For a worker joining a cluster, that version is the control plane
// Kubernetes version when the controller can read it; otherwise the Machine's version.
// +optional
ContentFormat FileContentFormat `json:"contentFormat,omitempty"`

// append specifies whether to append Content to existing file if Path exists.
// +optional
Append bool `json:"append,omitempty"`
Expand Down
16 changes: 16 additions & 0 deletions api/bootstrap/kubeadm/v1beta1/v1beta2_condition_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,19 @@ const (
// KubeadmConfigDataSecretNotAvailableV1Beta2Reason surfaces when the bootstrap secret is not available.
KubeadmConfigDataSecretNotAvailableV1Beta2Reason = clusterv1beta1.NotAvailableV1Beta2Reason
)

// KubeadmConfig's ControlPlaneKubernetesVersionAvailable condition and corresponding reasons that will be used in v1Beta2 API version.
const (
// KubeadmConfigControlPlaneKubernetesVersionAvailableV1Beta2Condition documents whether join Kubernetes version
// could be resolved from the Cluster's control plane reference when applicable.
KubeadmConfigControlPlaneKubernetesVersionAvailableV1Beta2Condition = "ControlPlaneKubernetesVersionAvailable"

// KubeadmConfigControlPlaneKubernetesVersionFromControlPlaneV1Beta2Reason surfaces when join version was read from the control plane.
KubeadmConfigControlPlaneKubernetesVersionFromControlPlaneV1Beta2Reason = "ControlPlaneKubernetesVersionFromControlPlane"

// KubeadmConfigControlPlaneKubernetesVersionFromMachineV1Beta2Reason surfaces when join version uses the Machine.
KubeadmConfigControlPlaneKubernetesVersionFromMachineV1Beta2Reason = "ControlPlaneKubernetesVersionFromMachine"

// KubeadmConfigControlPlaneKubernetesVersionResolutionFailedV1Beta2Reason surfaces when resolution failed.
KubeadmConfigControlPlaneKubernetesVersionResolutionFailedV1Beta2Reason = "ControlPlaneKubernetesVersionResolutionFailed"
)
2 changes: 2 additions & 0 deletions api/bootstrap/kubeadm/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions api/bootstrap/kubeadm/v1beta2/kubeadm_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ const (
KubeadmConfigDataSecretNotAvailableReason = clusterv1.NotAvailableReason
)

// KubeadmConfig's ControlPlaneKubernetesVersionAvailable condition and corresponding reasons.
// This condition is set when reconciling worker join bootstrap data: it reflects how the controller chose
// the Kubernetes version for join (from the control plane reference when possible, otherwise from the Machine).
const (
// KubeadmConfigControlPlaneKubernetesVersionAvailableCondition is true when the Kubernetes version for worker
// join was chosen successfully (from the control plane or from the Machine).
KubeadmConfigControlPlaneKubernetesVersionAvailableCondition = "ControlPlaneKubernetesVersionAvailable"

// KubeadmConfigControlPlaneKubernetesVersionFromControlPlaneReason surfaces when the Kubernetes version for
// worker join was read from the referenced control plane's spec.version.
KubeadmConfigControlPlaneKubernetesVersionFromControlPlaneReason = "FromControlPlane"

// KubeadmConfigControlPlaneKubernetesVersionFromMachineReason surfaces when the Kubernetes version for worker
// join uses the Machine's spec.version because the Cluster has no controlPlaneRef or the referenced control
// plane does not expose a version.
KubeadmConfigControlPlaneKubernetesVersionFromMachineReason = "FromMachine"

// KubeadmConfigControlPlaneKubernetesVersionResolutionFailedReason surfaces when the controller could not
// read the control plane object or its Kubernetes version while a controlPlaneRef is set.
KubeadmConfigControlPlaneKubernetesVersionResolutionFailedReason = "ResolutionFailed"
)

// EncryptionAlgorithmType can define an asymmetric encryption algorithm type.
// +kubebuilder:validation:Enum=ECDSA-P256;ECDSA-P384;RSA-2048;RSA-3072;RSA-4096
type EncryptionAlgorithmType string
Expand Down
30 changes: 29 additions & 1 deletion api/bootstrap/kubeadm/v1beta2/kubeadmconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var (
missingSecretNameMsg = "secret file source must specify non-empty secret name"
missingSecretKeyMsg = "secret file source must specify non-empty secret key"
pathConflictMsg = "path property must be unique among all files"
invalidFileContentFormatMsg = "contentFormat must be empty or go-template"
)

// KubeadmConfigSpec defines the desired state of KubeadmConfig.
Expand Down Expand Up @@ -217,6 +218,16 @@ func (c *KubeadmConfigSpec) validateFiles(pathPrefix *field.Path) field.ErrorLis
),
)
}
if file.ContentFormat != "" && file.ContentFormat != FileContentFormatGoTemplate {
allErrs = append(
allErrs,
field.Invalid(
pathPrefix.Child("files").Index(i).Child("contentFormat"),
file.ContentFormat,
invalidFileContentFormatMsg,
),
)
}
// n.b.: if we ever add types besides Secret as a ContentFrom
// Source, we must add webhook validation here for one of the
// sources being non-nil.
Expand Down Expand Up @@ -475,7 +486,7 @@ func (r *ContainerLinuxConfig) IsDefined() bool {
// +kubebuilder:validation:MinProperties=1
type KubeadmConfigStatus struct {
// conditions represents the observations of a KubeadmConfig's current state.
// Known condition types are Ready, DataSecretAvailable, CertificatesAvailable.
// Known condition types are Ready, DataSecretAvailable, CertificatesAvailable, ControlPlaneKubernetesVersionAvailable.
// +optional
// +listType=map
// +listMapKey=type
Expand Down Expand Up @@ -624,6 +635,16 @@ func init() {
// +kubebuilder:validation:Enum=base64;gzip;gzip+base64
type Encoding string

// FileContentFormat specifies how file content is interpreted after resolving content/contentFrom and before writing bootstrap data.
// +kubebuilder:validation:Enum="";go-template
type FileContentFormat string

const (
// FileContentFormatGoTemplate means content is rendered as a Go text/template with KubernetesVersion and other
// fields documented by the kubeadm bootstrap provider. The default empty value means content is used verbatim.
FileContentFormatGoTemplate FileContentFormat = "go-template"
)

const (
// Base64 implies the contents of the file are encoded as base64.
Base64 Encoding = "base64"
Expand Down Expand Up @@ -657,6 +678,13 @@ type File struct {
// +optional
Encoding Encoding `json:"encoding,omitempty"`

// contentFormat specifies how to interpret content after it is loaded (inline or from contentFrom).
// When set to "go-template", content is rendered as a Go text/template with data including KubernetesVersion
// (semver.String(), no "v" prefix). For a worker joining a cluster, that version is the control plane
// Kubernetes version when the controller can read it; otherwise the Machine's version.
// +optional
ContentFormat FileContentFormat `json:"contentFormat,omitempty"`

// append specifies whether to append Content to existing file if Path exists.
// +optional
Append *bool `json:"append,omitempty"`
Expand Down
16 changes: 16 additions & 0 deletions api/bootstrap/kubeadm/v1beta2/v1beta1_condition_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ const (
// an error while generating a data secret; those kind of errors are usually due to misconfigurations
// and user intervention is required to get them fixed.
DataSecretGenerationFailedV1Beta1Reason = "DataSecretGenerationFailed"

// ControlPlaneKubernetesVersionAvailableV1Beta1Condition documents whether the controller could resolve the
// Kubernetes version used for worker join from the Cluster's control plane reference (when applicable).
ControlPlaneKubernetesVersionAvailableV1Beta1Condition clusterv1.ConditionType = "ControlPlaneKubernetesVersionAvailable"

// ControlPlaneKubernetesVersionResolutionFailedV1Beta1Reason (Severity=Warning) documents a failure to read
// the control plane object or its Kubernetes version while resolving join bootstrap data.
ControlPlaneKubernetesVersionResolutionFailedV1Beta1Reason = "ControlPlaneKubernetesVersionResolutionFailed"

// ControlPlaneKubernetesVersionFromControlPlaneV1Beta1Reason documents that the Kubernetes version for worker
// join was read from the Cluster's control plane reference.
ControlPlaneKubernetesVersionFromControlPlaneV1Beta1Reason = "ControlPlaneKubernetesVersionFromControlPlane"

// ControlPlaneKubernetesVersionFromMachineV1Beta1Reason documents that the Kubernetes version for worker join
// uses the Machine's spec.version because the control plane reference is unset or does not expose a version.
ControlPlaneKubernetesVersionFromMachineV1Beta1Reason = "ControlPlaneKubernetesVersionFromMachine"
)

const (
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions bootstrap/kubeadm/config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,12 @@ rules:
- get
- list
- watch
- apiGroups:
- controlplane.cluster.x-k8s.io
resources:
- kubeadmcontrolplanes
- kubeadmcontrolplanes/status
verbs:
- get
- list
- watch
63 changes: 63 additions & 0 deletions bootstrap/kubeadm/internal/bootstrapfiles/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2026 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package bootstrapfiles contains helpers for KubeadmConfig spec.files processing.
package bootstrapfiles

import (
"bytes"
"text/template"

"github.com/blang/semver/v4"
"github.com/pkg/errors"

bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2"
)

// TemplateData is passed to Go text/template when a KubeadmConfig spec.files entry uses contentFormat "go-template".
type TemplateData struct {
// KubernetesVersion is the effective Kubernetes version for bootstrap data (semver.String(), no "v" prefix).
// For a worker Machine joining a cluster, this is the control plane Kubernetes version when the controller
// can read it; otherwise the Machine's version.
KubernetesVersion string
}

// DataFromVersion returns template data using semver.Version.String() (no "v" prefix).
func DataFromVersion(v semver.Version) TemplateData {
return TemplateData{KubernetesVersion: v.String()}
}

// RenderTemplates renders go-template file contents and clears contentFormat on those entries.
func RenderTemplates(files []bootstrapv1.File, data TemplateData) ([]bootstrapv1.File, error) {
out := make([]bootstrapv1.File, len(files))
copy(out, files)
for i := range out {
if out[i].ContentFormat != bootstrapv1.FileContentFormatGoTemplate {
continue
}
tpl, err := template.New(out[i].Path).Option("missingkey=error").Parse(out[i].Content)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse go-template for file %q", out[i].Path)
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil {
return nil, errors.Wrapf(err, "failed to execute go-template for file %q", out[i].Path)
}
out[i].Content = buf.String()
out[i].ContentFormat = ""
}
return out, nil
}
Loading