diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go index c31c379e3..fc6f17047 100644 --- a/api/v1alpha1/zz_generated.conversion.go +++ b/api/v1alpha1/zz_generated.conversion.go @@ -788,7 +788,9 @@ func autoConvert_v1alpha2_LinodeMachineSpec_To_v1alpha1_LinodeMachineSpec(in *v1 out.FirewallID = in.FirewallID out.OSDisk = (*InstanceDisk)(unsafe.Pointer(in.OSDisk)) out.DataDisks = *(*map[string]*InstanceDisk)(unsafe.Pointer(&in.DataDisks)) + // WARNING: in.DiskEncryption requires manual conversion: does not exist in peer-type out.CredentialsRef = (*v1.SecretReference)(unsafe.Pointer(in.CredentialsRef)) + // WARNING: in.Configuration requires manual conversion: does not exist in peer-type // WARNING: in.PlacementGroupRef requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1alpha2/linodemachine_types.go b/api/v1alpha2/linodemachine_types.go index 305632634..b3b52792b 100644 --- a/api/v1alpha2/linodemachine_types.go +++ b/api/v1alpha2/linodemachine_types.go @@ -74,6 +74,10 @@ type LinodeMachineSpec struct { // DataDisks is a map of any additional disks to add to an instance, // The sum of these disks + the OSDisk must not be more than allowed on a linodes plan DataDisks map[string]*InstanceDisk `json:"dataDisks,omitempty"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +kubebuilder:validation:Enum=enabled;disabled + // DiskEncryption determines if the disks of the instance should be encrypted. + DiskEncryption string `json:"diskEncryption,omitempty"` // CredentialsRef is a reference to a Secret that contains the credentials // to use for provisioning this machine. If not supplied then these @@ -84,6 +88,10 @@ type LinodeMachineSpec struct { // +optional CredentialsRef *corev1.SecretReference `json:"credentialsRef,omitempty"` + // Configuration is the Akamai instance configuration OS, + // if not specified this defaults to the default configuration associated to the instance. + Configuration *InstanceConfiguration `json:"configuration,omitempty"` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" // +optional // PlacementGroupRef is a reference to a placement group object. This makes the linode to be launched in that specific group. @@ -110,6 +118,12 @@ type InstanceMetadataOptions struct { UserData string `json:"userData,omitempty"` } +// InstanceConfiguration defines the instance configuration +type InstanceConfiguration struct { + // Kernel is a Kernel ID to boot a Linode with. (e.g linode/latest-64bit) + Kernel string `json:"kernel,omitempty"` +} + // InstanceConfigInterfaceCreateOptions defines network interface config type InstanceConfigInterfaceCreateOptions struct { IPAMAddress string `json:"ipamAddress,omitempty"` diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index a0d684119..fa9e821bc 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -58,6 +58,21 @@ func (in *InstanceConfigInterfaceCreateOptions) DeepCopy() *InstanceConfigInterf return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceConfiguration) DeepCopyInto(out *InstanceConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceConfiguration. +func (in *InstanceConfiguration) DeepCopy() *InstanceConfiguration { + if in == nil { + return nil + } + out := new(InstanceConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InstanceDisk) DeepCopyInto(out *InstanceDisk) { *out = *in @@ -422,6 +437,11 @@ func (in *LinodeMachineSpec) DeepCopyInto(out *LinodeMachineSpec) { *out = new(v1.SecretReference) **out = **in } + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = new(InstanceConfiguration) + **out = **in + } if in.PlacementGroupRef != nil { in, out := &in.PlacementGroupRef, &out.PlacementGroupRef *out = new(v1.ObjectReference) diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml index bab1a2301..be1ce5d5c 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml @@ -456,6 +456,16 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + configuration: + description: |- + Configuration is the Akamai instance configuration OS, + if not specified this defaults to the default configuration associated to the instance. + properties: + kernel: + description: Kernel is a Kernel ID to boot a Linode with. (e.g + linode/latest-64bit) + type: string + type: object credentialsRef: description: |- CredentialsRef is a reference to a Secret that contains the credentials @@ -511,6 +521,16 @@ spec: DataDisks is a map of any additional disks to add to an instance, The sum of these disks + the OSDisk must not be more than allowed on a linodes plan type: object + diskEncryption: + description: DiskEncryption determines if the disks of the instance + should be encrypted. + enum: + - enabled + - disabled + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf firewallID: type: integer x-kubernetes-validations: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml index 61dea0dfb..525a03fea 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml @@ -320,6 +320,16 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + configuration: + description: |- + Configuration is the Akamai instance configuration OS, + if not specified this defaults to the default configuration associated to the instance. + properties: + kernel: + description: Kernel is a Kernel ID to boot a Linode with. + (e.g linode/latest-64bit) + type: string + type: object credentialsRef: description: |- CredentialsRef is a reference to a Secret that contains the credentials @@ -376,6 +386,16 @@ spec: DataDisks is a map of any additional disks to add to an instance, The sum of these disks + the OSDisk must not be more than allowed on a linodes plan type: object + diskEncryption: + description: DiskEncryption determines if the disks of the + instance should be encrypted. + enum: + - enabled + - disabled + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf firewallID: type: integer x-kubernetes-validations: diff --git a/controller/linodemachine_controller.go b/controller/linodemachine_controller.go index ec51844b9..068c11df4 100644 --- a/controller/linodemachine_controller.go +++ b/controller/linodemachine_controller.go @@ -337,6 +337,7 @@ func (r *LinodeMachineReconciler) reconcileCreate( return r.reconcileInstanceCreate(ctx, logger, machineScope, linodeInstance) } +//nolint:cyclop,gocognit // It is ok for the moment but need larger refactor. func (r *LinodeMachineReconciler) reconcileInstanceCreate( ctx context.Context, logger logr.Logger, @@ -357,6 +358,18 @@ func (r *LinodeMachineReconciler) reconcileInstanceCreate( conditions.MarkTrue(machineScope.LinodeMachine, ConditionPreflightConfigured) } + if machineScope.LinodeMachine.Spec.Configuration != nil && machineScope.LinodeMachine.Spec.Configuration.Kernel != "" { + instanceConfig, err := r.getDefaultInstanceConfig(ctx, machineScope, linodeInstance.ID) + if err != nil { + logger.Error(err, "Failed to get default instance configuration") + return ctrl.Result{}, err + } + + if _, err := machineScope.LinodeClient.UpdateInstanceConfig(ctx, linodeInstance.ID, instanceConfig.ID, linodego.InstanceConfigUpdateOptions{Kernel: machineScope.LinodeMachine.Spec.Configuration.Kernel}); err != nil { + return ctrl.Result{}, err + } + } + if !reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightBootTriggered) { if err := machineScope.LinodeClient.BootInstance(ctx, linodeInstance.ID, 0); err != nil && !strings.HasSuffix(err.Error(), "already booted.") { logger.Error(err, "Failed to boot instance") @@ -529,16 +542,14 @@ func (r *LinodeMachineReconciler) resizeRootDisk( if reconciler.ConditionTrue(machineScope.LinodeMachine, ConditionPreflightRootDiskResized) { return nil } - // get the default instance config - configs, err := machineScope.LinodeClient.ListInstanceConfigs(ctx, linodeInstanceID, &linodego.ListOptions{}) - if err != nil || len(configs) == 0 { - logger.Error(err, "Failed to list instance configs") - conditions.MarkFalse(machineScope.LinodeMachine, ConditionPreflightRootDiskResized, string(cerrs.CreateMachineError), clusterv1.ConditionSeverityWarning, err.Error()) + instanceConfig, err := r.getDefaultInstanceConfig(ctx, machineScope, linodeInstanceID) + if err != nil { + logger.Error(err, "Failed to get default instance configuration") + conditions.MarkFalse(machineScope.LinodeMachine, ConditionPreflightRootDiskResized, string(cerrs.CreateMachineError), clusterv1.ConditionSeverityWarning, err.Error()) return err } - instanceConfig := configs[0] if instanceConfig.Devices.SDA == nil { conditions.MarkFalse(machineScope.LinodeMachine, ConditionPreflightRootDiskResized, string(cerrs.CreateMachineError), clusterv1.ConditionSeverityWarning, "root disk not yet ready") @@ -779,3 +790,16 @@ func (r *LinodeMachineReconciler) SetupWithManager(mgr ctrl.Manager, options crc func (r *LinodeMachineReconciler) TracedClient() client.Client { return wrappedruntimeclient.NewRuntimeClientWithTracing(r.Client, wrappedruntimeclient.DefaultDecorator()) } + +func (r *LinodeMachineReconciler) getDefaultInstanceConfig( + ctx context.Context, + machineScope *scope.MachineScope, + linodeInstanceID int, +) (linodego.InstanceConfig, error) { + configs, err := machineScope.LinodeClient.ListInstanceConfigs(ctx, linodeInstanceID, &linodego.ListOptions{}) + if err != nil || len(configs) == 0 { + return linodego.InstanceConfig{}, fmt.Errorf("failing to list instance configurations: %w", err) + } + + return configs[0], nil +} diff --git a/controller/linodemachine_controller_test.go b/controller/linodemachine_controller_test.go index 7af1b3ebb..3eb0e1984 100644 --- a/controller/linodemachine_controller_test.go +++ b/controller/linodemachine_controller_test.go @@ -107,9 +107,10 @@ var _ = Describe("create", Label("machine", "create"), func() { UID: "12345", }, Spec: infrav1alpha2.LinodeMachineSpec{ - InstanceID: ptr.To(0), - Type: "g6-nanode-1", - Image: rutil.DefaultMachineControllerLinodeImage, + InstanceID: ptr.To(0), + Type: "g6-nanode-1", + Image: rutil.DefaultMachineControllerLinodeImage, + DiskEncryption: string(linodego.InstanceDiskEncryptionEnabled), }, } reconciler = &LinodeMachineReconciler{ @@ -141,7 +142,7 @@ var _ = Describe("create", Label("machine", "create"), func() { getRegion := mockLinodeClient.EXPECT(). GetRegion(ctx, gomock.Any()). After(listInst). - Return(&linodego.Region{Capabilities: []string{"Metadata"}}, nil) + Return(&linodego.Region{Capabilities: []string{linodego.CapabilityMetadata, linodego.CapabilityDiskEncryption}}, nil) getImage := mockLinodeClient.EXPECT(). GetImage(ctx, gomock.Any()). After(getRegion). @@ -227,7 +228,7 @@ var _ = Describe("create", Label("machine", "create"), func() { getRegion := mockLinodeClient.EXPECT(). GetRegion(ctx, gomock.Any()). After(listInst). - Return(&linodego.Region{Capabilities: []string{"Metadata"}}, nil) + Return(&linodego.Region{Capabilities: []string{linodego.CapabilityMetadata, linodego.CapabilityDiskEncryption}}, nil) getImage := mockLinodeClient.EXPECT(). GetImage(ctx, gomock.Any()). After(getRegion). @@ -309,7 +310,7 @@ var _ = Describe("create", Label("machine", "create"), func() { getRegion := mockLinodeClient.EXPECT(). GetRegion(ctx, gomock.Any()). After(listInst). - Return(&linodego.Region{Capabilities: []string{"Metadata"}}, nil) + Return(&linodego.Region{Capabilities: []string{linodego.CapabilityMetadata, linodego.CapabilityDiskEncryption}}, nil) getImage := mockLinodeClient.EXPECT(). GetImage(ctx, gomock.Any()). After(getRegion). @@ -460,7 +461,7 @@ var _ = Describe("create", Label("machine", "create"), func() { getRegion := mockLinodeClient.EXPECT(). GetRegion(ctx, gomock.Any()). After(listInst). - Return(&linodego.Region{Capabilities: []string{"Metadata"}}, nil) + Return(&linodego.Region{Capabilities: []string{linodego.CapabilityMetadata, linodego.CapabilityDiskEncryption}}, nil) getImage := mockLinodeClient.EXPECT(). GetImage(ctx, gomock.Any()). After(getRegion). diff --git a/docs/src/topics/flavors/flatcar.md b/docs/src/topics/flavors/flatcar.md new file mode 100644 index 000000000..56298cdc8 --- /dev/null +++ b/docs/src/topics/flavors/flatcar.md @@ -0,0 +1,56 @@ +# Flatcar + +This flavor supports provisioning k8s clusters outside of VPC using [Flatcar][flatcar] as a base OS. It uses kubeadm for +setting up control plane and uses cilium with VXLAN for pod networking. + +## Specification +| Supported Control Plane | CNI | Default OS | Installs ClusterClass | IPv4 | IPv6 | +|-------------------------|--------|--------------|-----------------------|------|------| +| kubeadm | Cilium | Flatcar | No | Yes | No | + +## Notes +This flavor is identical to the default flavor with the exception that it provisions +k8s clusters without VPC using [Flatcar][flatcar] as a base OS. Since it runs outside of VPC, native routing is not +supported in this flavor and it uses VXLAN for pod to pod communication. + +## Usage + +### Initialization + +Before generating the cluster configuration, it is required to initialize the management cluster with [Ignition][ignition] support to provision Flatcar nodes: + +```bash +export EXP_KUBEADM_BOOTSTRAP_FORMAT_IGNITION=true +clusterctl init --infrastructure linode-linode --addon helm +``` + +### Import the Flatcar image + +Flatcar is not officially provided by Akamai/Linode so it is required to import a Flatcar image. Akamai support is available on Flatcar since the release [4012.0.0][release-4012]: all releases equal or greater than this major release will fit. + +To import the image, it is recommended to follow this documentation: https://www.flatcar.org/docs/latest/installing/community-platforms/akamai/#importing-an-image + +By following this import step, you will get the Flatcar image ID stored into `IMAGE_ID`. + +### Configure and deploy the workload cluster + +1. Set the Flatcar image name from the previous step: + ```bash + export FLATCAR_IMAGE_NAME="${IMAGE_ID}" + ``` + +2. Generate cluster yaml + ```bash + clusterctl generate cluster test-cluster \ + --kubernetes-version v1.29.1 \ + --infrastructure linode-linode \ + --flavor kubeadm-flatcar > test-cluster.yaml + ``` +2. Apply cluster yaml + ```bash + kubectl apply -f test-cluster.yaml + ``` + +[flatcar]: https://www.flatcar.org/ +[ignition]: https://coreos.github.io/ignition/ +[release-4012]: https://www.flatcar.org/releases#release-4012.0.0 diff --git a/docs/src/topics/getting-started.md b/docs/src/topics/getting-started.md index e25c98770..c8ceb6357 100644 --- a/docs/src/topics/getting-started.md +++ b/docs/src/topics/getting-started.md @@ -32,9 +32,9 @@ export LINODE_MACHINE_TYPE=g6-standard-2 For Regions and Images that do not yet support Akamai's cloud-init datasource CAPL will automatically use a stackscript shim to provision the node. If you are using a custom image ensure the [cloud_init](https://www.linode.com/docs/api/images/#image-create) flag is set correctly on it ``` -```admonish warning -By default, clusters are provisioned within VPC. For Regions which do not have [VPC support](https://www.linode.com/docs/products/networking/vpc/#availability) yet, use the [VPCLess](./flavors/vpcless.md) flavor to have clusters provisioned. -``` +~~~admonish warning +By default, clusters are provisioned within VPC with disk encryption enabled. For Regions which do not have [VPC support](https://www.linode.com/docs/products/networking/vpc/#availability) yet, use the [VPCLess](./flavors/vpcless.md) flavor to have clusters provisioned. For disabling disk encryption, set `spec.template.spec.diskEncryption=disabled` in your generated LinodeMachineTemplate resources when creating a CAPL cluster. +~~~ ## Install CAPL on your management cluster ```admonish warning diff --git a/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-capi-resources.yaml b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-capi-resources.yaml new file mode 100644 index 000000000..0c8d0fc15 --- /dev/null +++ b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-capi-resources.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-controller-manager + namespace: capi-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capl-controller-manager + namespace: capl-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-kubeadm-bootstrap-controller-manager + namespace: kubeadm-bootstrap-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-kubeadm-control-plane-controller-manager + namespace: kubeadm-control-plane-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: caaph-controller-manager + namespace: caaph-system +status: + availableReplicas: 1 diff --git a/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-daemonsets.yaml b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-daemonsets.yaml new file mode 100644 index 000000000..fa12ba942 --- /dev/null +++ b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-daemonsets.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: ccm-linode + namespace: kube-system +status: + currentNumberScheduled: 1 + desiredNumberScheduled: 1 + numberAvailable: 1 + numberMisscheduled: 0 + numberReady: 1 +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: cilium + namespace: kube-system +status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: csi-linode-node + namespace: kube-system +status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 diff --git a/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-deployments.yaml b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-deployments.yaml new file mode 100644 index 000000000..c1d71094f --- /dev/null +++ b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-deployments.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cilium-operator + namespace: kube-system +status: + availableReplicas: 2 + readyReplicas: 2 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/part-of: cilium + namespace: kube-system +status: + readyReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coredns + namespace: kube-system +status: + availableReplicas: 2 + readyReplicas: 2 diff --git a/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-resources.yaml b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-resources.yaml new file mode 100644 index 000000000..a1575a44f --- /dev/null +++ b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/assert-child-cluster-resources.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 +kind: LinodeMachine +metadata: + labels: + cluster.x-k8s.io/cluster-name: ($cluster) +spec: + region: (env('LINODE_REGION')) + type: g6-standard-2 +status: + ready: true + instanceState: running +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Machine +metadata: + labels: + cluster.x-k8s.io/cluster-name: ($cluster) +spec: + clusterName: ($cluster) +status: + bootstrapReady: true + infrastructureReady: true +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineDeployment +metadata: + labels: + cluster.x-k8s.io/cluster-name: ($cluster) +spec: + clusterName: ($cluster) + replicas: 1 +status: + readyReplicas: 1 + unavailableReplicas: 0 + availableReplicas: 1 +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlane +metadata: + labels: + cluster.x-k8s.io/cluster-name: ($cluster) +status: + readyReplicas: 1 + unavailableReplicas: 0 + ready: true +--- +apiVersion: addons.cluster.x-k8s.io/v1alpha1 +kind: HelmReleaseProxy +metadata: + labels: + cluster.x-k8s.io/cluster-name: ($cluster) +status: + conditions: + - type: Ready + status: "True" + - type: ClusterAvailable + status: "True" + - type: HelmReleaseReady + status: "True" + status: deployed diff --git a/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/chainsaw-test.yaml b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/chainsaw-test.yaml new file mode 100755 index 000000000..a3b84934f --- /dev/null +++ b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/chainsaw-test.yaml @@ -0,0 +1,228 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + creationTimestamp: null + name: flatcar-capl-cluster + # Labels to allow the test to be triggered based on selector flag + labels: + all: + kubeadm: + flavors: + flatcar: +spec: + bindings: + # A short identifier for the E2E test run + - name: run + value: (join('-', ['e2e', 'flatcar-cluster', env('GIT_REF')])) + - name: cluster + # Format the cluster name + value: (trim((truncate(($run), `32`)), '-')) + template: true + steps: + - name: Check if CAPI provider resources exist + try: + - assert: + file: assert-capi-resources.yaml + - name: Upload Flatcar image + try: + - script: + env: + - name: TARGET_API + value: api.linode.com + - name: TARGET_API_VERSION + value: v4 + - name: URI + value: images/upload + - name: LINODE_REGION + value: (env('LINODE_REGION')) + content: | + set -e + # Get the latest version on Alpha channel. + # NOTE: This can be changed to Beta or Stable when Akamai support will come on these channels. + curl -fsSL --remote-name \ + https://alpha.release.flatcar-linux.net/amd64-usr/current/flatcar_production_akamai_image.bin.gz + + res=$(curl -s --request POST \ + --url "https://${TARGET_API}/${TARGET_API_VERSION}/${URI}" \ + --data '{"region":"'${LINODE_REGION}'","cloud_init":true,"label":"flatcar-alpha"}' \ + --header "Authorization: Bearer ${LINODE_TOKEN}" \ + --header "accept: application/json" \ + --header "content-type: application/json") + + UPLOAD_URL=$(echo "${res}" | jq -r .upload_to) + IMAGE_ID=$(echo "${res}" | jq -r .image.id) + echo "${IMAGE_ID}" > image-id + + curl -s --request PUT \ + "${UPLOAD_URL}" \ + --header "Content-Type: application/octet-stream" \ + --upload-file flatcar_production_akamai_image.bin.gz \ + --progress-bar \ + --output /dev/null + check: + ($error): ~ + - name: Generate cluster using clusterctl + try: + - script: + env: + - name: CLUSTER + value: ($cluster) + - name: NAMESPACE + value: ($namespace) + - name: CLUSTERCTL_CONFIG + value: (env('CLUSTERCTL_CONFIG')) + content: | + set -e + export FLATCAR_IMAGE_NAME=$(cat image-id) + clusterctl generate cluster $CLUSTER -n $NAMESPACE \ + --kubernetes-version v1.29.5 \ + --infrastructure local-linode:v0.0.0 \ + --control-plane-machine-count 1 --worker-machine-count 1 \ + --flavor kubeadm-flatcar \ + --config ${CLUSTERCTL_CONFIG:=${HOME}/.cluster-api/clusterctl.yaml} > flatcar-cluster.yaml + check: + ($error == null): true + - name: Apply generated cluster yaml + try: + - apply: + file: flatcar-cluster.yaml + - assert: + file: assert-child-cluster-resources.yaml + catch: + - describe: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: LinodeMachine + - describe: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Machine + - describe: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: MachineDeployment + - describe: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlane + - describe: + apiVersion: addons.cluster.x-k8s.io/v1alpha1 + kind: HelmReleaseProxy + - name: Check if the linodes are created + try: + - script: + env: + - name: TARGET_API + value: api.linode.com + - name: TARGET_API_VERSION + value: v4beta + - name: URI + value: linode/instances + - name: FILTER + value: (to_string({"tags":($cluster)})) + content: | + set -e + curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "X-Filter: $FILTER" \ + -H "Content-Type: application/json" \ + "https://$TARGET_API/$TARGET_API_VERSION/$URI" + check: + ($error): ~ + (json_parse($stdout)): + results: 2 + - name: Get child cluster kubeconfig + try: + - script: + env: + - name: CLUSTER + value: ($cluster) + - name: NAMESPACE + value: ($namespace) + - name: CLUSTERCTL_CONFIG + value: (env('CLUSTERCTL_CONFIG')) + content: | + set -e + clusterctl get kubeconfig $CLUSTER -n $NAMESPACE > flatcar-cluster-kubeconfig.yaml + check: + ($error == null): true + - clusters: + flatcar-cluster: + kubeconfig: ./flatcar-cluster-kubeconfig.yaml + name: Check child cluster resources + try: + - assert: + cluster: flatcar-cluster + file: assert-child-cluster-deployments.yaml + - assert: + cluster: flatcar-cluster + file: assert-child-cluster-daemonsets.yaml + catch: + - describe: + cluster: flatcar-cluster + apiVersion: apps/v1 + kind: Deployment + namespace: kube-system + - describe: + cluster: flatcar-cluster + apiVersion: apps/v1 + kind: DaemonSet + namespace: kube-system + - name: Delete child cluster + try: + - delete: + ref: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Cluster + name: ($cluster) + - error: + file: check-child-cluster-deleted.yaml + - name: Check if the linodes are deleted + try: + - script: + env: + - name: TARGET_API + value: api.linode.com + - name: TARGET_API_VERSION + value: v4beta + - name: URI + value: linode/instances + - name: FILTER + value: (to_string({"tags":($cluster)})) + content: | + set -e + curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "X-Filter: $FILTER" \ + -H "Content-Type: application/json" \ + "https://$TARGET_API/$TARGET_API_VERSION/$URI" + check: + ($error): ~ + (json_parse($stdout)): + results: 0 + - name: Delete generated child cluster manifest yaml + try: + - script: + content: | + rm -f flatcar-cluster.yaml + rm -f flatcar-cluster-kubeconfig.yaml + check: + ($error == null): true + - name: Delete Flatcar image + try: + - script: + env: + - name: TARGET_API + value: api.linode.com + - name: TARGET_API_VERSION + value: v4 + - name: URI + value: images + - name: LINODE_REGION + value: (env('LINODE_REGION')) + content: | + set -e + # Get the latest version on Alpha channel. + # NOTE: This can be changed to Beta or Stable when Akamai support will come on these channels. + IMAGE_ID=$(cat image-id) + curl -s --request DELETE \ + --url "https://${TARGET_API}/${TARGET_API_VERSION}/${URI}/${IMAGE_ID}" \ + --header "Authorization: Bearer ${LINODE_TOKEN}" \ + --header "accept: application/json" diff --git a/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/check-child-cluster-deleted.yaml b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/check-child-cluster-deleted.yaml new file mode 100644 index 000000000..81cd1fd17 --- /dev/null +++ b/e2e/capl-cluster-flavors/kubeadm-flatcar-vpcless-capl-cluster/check-child-cluster-deleted.yaml @@ -0,0 +1,5 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 +kind: LinodeMachine +metadata: + labels: + cluster.x-k8s.io/cluster-name: ($cluster) diff --git a/go.mod b/go.mod index 32f60d500..c26dd9fc8 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/linode/linodego v1.38.0 - github.com/onsi/ginkgo/v2 v2.19.0 - github.com/onsi/gomega v1.33.1 + github.com/onsi/ginkgo/v2 v2.19.1 + github.com/onsi/gomega v1.34.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 go.opentelemetry.io/otel v1.28.0 diff --git a/go.sum b/go.sum index 67c1054d0..3b4d3fce1 100644 --- a/go.sum +++ b/go.sum @@ -144,11 +144,11 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= +github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os= +github.com/onsi/gomega v1.34.0/go.mod h1:MIKI8c+f+QLWk+hxbePD4i0LMJSExPaZOVfkoex4cAo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/hack/manifests/kubeadm.yaml b/hack/manifests/kubeadm.yaml index 7dda7acd5..bbef71fce 100644 --- a/hack/manifests/kubeadm.yaml +++ b/hack/manifests/kubeadm.yaml @@ -4,6 +4,10 @@ kind: BootstrapProvider metadata: name: kubeadm namespace: kubeadm-bootstrap-system +spec: + manager: + featureGates: + KubeadmBootstrapFormatIgnition: true --- apiVersion: operator.cluster.x-k8s.io/v1alpha2 kind: ControlPlaneProvider @@ -14,3 +18,4 @@ spec: manager: featureGates: ClusterTopology: true + KubeadmBootstrapFormatIgnition: true diff --git a/templates/flavors/kubeadm/flatcar/kustomization.yaml b/templates/flavors/kubeadm/flatcar/kustomization.yaml new file mode 100644 index 000000000..841f5dc01 --- /dev/null +++ b/templates/flavors/kubeadm/flatcar/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../vpcless +patches: + - path: patch-flatcar.yaml diff --git a/templates/flavors/kubeadm/flatcar/patch-flatcar.yaml b/templates/flavors/kubeadm/flatcar/patch-flatcar.yaml new file mode 100644 index 000000000..e5c1c4e32 --- /dev/null +++ b/templates/flavors/kubeadm/flatcar/patch-flatcar.yaml @@ -0,0 +1,119 @@ +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlane +metadata: + name: "${CLUSTER_NAME}-control-plane" +spec: + replicas: ${CONTROL_PLANE_MACHINE_COUNT} + kubeadmConfigSpec: + joinConfiguration: + nodeRegistration: + name: $${COREOS_AKAMAI_INSTANCE_LABEL} + kubeletExtraArgs: + provider-id: linode://$${COREOS_AKAMAI_INSTANCE_ID} + initConfiguration: + nodeRegistration: + name: $${COREOS_AKAMAI_INSTANCE_LABEL} + kubeletExtraArgs: + provider-id: linode://$${COREOS_AKAMAI_INSTANCE_ID} + format: ignition + ignition: + containerLinuxConfig: + additionalConfig: | + storage: + links: + - path: /etc/extensions/kubernetes.raw + hard: false + target: /opt/extensions/kubernetes/kubernetes-${KUBERNETES_VERSION}-x86-64.raw + files: + - path: /opt/extensions/kubernetes/kubernetes-${KUBERNETES_VERSION}-x86-64.raw + contents: + remote: + url: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-${KUBERNETES_VERSION}-x86-64.raw + systemd: + units: + - name: kubeadm.service + enabled: true + dropins: + - name: 10-flatcar.conf + contents: | + [Unit] + Requires=containerd.service coreos-metadata.service + After=containerd.service coreos-metadata.service + [Service] + EnvironmentFile=/run/metadata/flatcar + preKubeadmCommands: + - export COREOS_AKAMAI_INSTANCE_LABEL=$${COREOS_AKAMAI_INSTANCE_LABEL} + - export COREOS_AKAMAI_INSTANCE_ID=$${COREOS_AKAMAI_INSTANCE_ID} + - export COREOS_AKAMAI_PRIVATE_IPV4_0=$${COREOS_AKAMAI_PRIVATE_IPV4_0} + - ip addr add "$${COREOS_AKAMAI_PRIVATE_IPV4_0}" dev eth0 + - envsubst < /etc/kubeadm.yml > /etc/kubeadm.yml.tmp + - mv /etc/kubeadm.yml.tmp /etc/kubeadm.yml +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: ${CLUSTER_NAME}-md-0 +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + name: $${COREOS_AKAMAI_INSTANCE_LABEL} + kubeletExtraArgs: + provider-id: linode://$${COREOS_AKAMAI_INSTANCE_ID} + preKubeadmCommands: + - export COREOS_AKAMAI_INSTANCE_LABEL=$${COREOS_AKAMAI_INSTANCE_LABEL} + - export COREOS_AKAMAI_INSTANCE_ID=$${COREOS_AKAMAI_INSTANCE_ID} + - export COREOS_AKAMAI_PRIVATE_IPV4_0=$${COREOS_AKAMAI_PRIVATE_IPV4_0} + - ip addr add "$${COREOS_AKAMAI_PRIVATE_IPV4_0}" dev eth0 + - envsubst < /etc/kubeadm.yml > /etc/kubeadm.yml.tmp + - mv /etc/kubeadm.yml.tmp /etc/kubeadm.yml + format: ignition + ignition: + containerLinuxConfig: + additionalConfig: | + storage: + links: + - path: /etc/extensions/kubernetes.raw + hard: false + target: /opt/extensions/kubernetes/kubernetes-${KUBERNETES_VERSION}-x86-64.raw + files: + - path: /opt/extensions/kubernetes/kubernetes-${KUBERNETES_VERSION}-x86-64.raw + contents: + remote: + url: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-${KUBERNETES_VERSION}-x86-64.raw + systemd: + units: + - name: kubeadm.service + enabled: true + dropins: + - name: 10-flatcar.conf + contents: | + [Unit] + Requires=containerd.service coreos-metadata.service + After=containerd.service coreos-metadata.service + [Service] + EnvironmentFile=/run/metadata/flatcar +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeMachineTemplate +metadata: + name: ${CLUSTER_NAME}-md-0 +spec: + template: + spec: + image: ${FLATCAR_IMAGE_NAME} + configuration: + kernel: linode/direct-disk +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeMachineTemplate +metadata: + name: ${CLUSTER_NAME}-control-plane +spec: + template: + spec: + image: ${FLATCAR_IMAGE_NAME} + configuration: + kernel: linode/direct-disk diff --git a/templates/infra/linodeMachineTemplate.yaml b/templates/infra/linodeMachineTemplate.yaml index 16b213cf5..38b9cd634 100644 --- a/templates/infra/linodeMachineTemplate.yaml +++ b/templates/infra/linodeMachineTemplate.yaml @@ -9,6 +9,7 @@ spec: image: ${LINODE_OS:="linode/ubuntu22.04"} type: ${LINODE_CONTROL_PLANE_MACHINE_TYPE} region: ${LINODE_REGION} + # diskEncryption: disabled interfaces: - purpose: public authorizedKeys: @@ -25,6 +26,7 @@ spec: image: ${LINODE_OS:="linode/ubuntu22.04"} type: ${LINODE_MACHINE_TYPE} region: ${LINODE_REGION} + # diskEncryption: disabled interfaces: - purpose: public authorizedKeys: