diff --git a/.golangci.yml b/.golangci.yml index 3e762f76d..471f52d15 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -202,6 +202,7 @@ issues: - path: _test(ing)?\.go linters: - gocyclo + - maintidx - errcheck - dupl - gosec diff --git a/api/v1alpha1/linodemachine_types.go b/api/v1alpha1/linodemachine_types.go index 72e5e5ee0..f1af9469f 100644 --- a/api/v1alpha1/linodemachine_types.go +++ b/api/v1alpha1/linodemachine_types.go @@ -51,7 +51,7 @@ type LinodeMachineSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" AuthorizedUsers []string `json:"authorizedUsers,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" - BackupID int `json:"backupId,omitempty"` + BackupID int `json:"backupID,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" Image string `json:"image,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" @@ -59,11 +59,11 @@ type LinodeMachineSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" BackupsEnabled bool `json:"backupsEnabled,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" - PrivateIP bool `json:"privateIp,omitempty"` + PrivateIP *bool `json:"privateIP,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" Tags []string `json:"tags,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" - FirewallID int `json:"firewallId,omitempty"` + FirewallID int `json:"firewallID,omitempty"` // CredentialsRef is a reference to a Secret that contains the credentials // to use for provisioning this machine. If not supplied then these diff --git a/api/v1alpha1/linodeobjectstoragebucket_types.go b/api/v1alpha1/linodeobjectstoragebucket_types.go index 371e03555..b55244fac 100644 --- a/api/v1alpha1/linodeobjectstoragebucket_types.go +++ b/api/v1alpha1/linodeobjectstoragebucket_types.go @@ -92,7 +92,7 @@ type LinodeObjectStorageBucketStatus struct { } // +kubebuilder:object:root=true -// +kubebuilder:resource:path=linodeobjectstoragebuckets,scope=Namespaced,shortName=lobj +// +kubebuilder:resource:path=linodeobjectstoragebuckets,scope=Namespaced,categories=cluster-api,shortName=lobj // +kubebuilder:subresource:status // +kubebuilder:metadata:labels="clusterctl.cluster.x-k8s.io/move-hierarchy=true" // +kubebuilder:printcolumn:name="Label",type="string",JSONPath=".spec.label",description="The name of the bucket" @@ -116,7 +116,7 @@ func (b *LinodeObjectStorageBucket) SetConditions(conditions clusterv1.Condition b.Status.Conditions = conditions } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // LinodeObjectStorageBucketList contains a list of LinodeObjectStorageBucket type LinodeObjectStorageBucketList struct { diff --git a/api/v1alpha1/linodevpc_types.go b/api/v1alpha1/linodevpc_types.go index 353befb53..3eff645cb 100644 --- a/api/v1alpha1/linodevpc_types.go +++ b/api/v1alpha1/linodevpc_types.go @@ -104,7 +104,9 @@ type LinodeVPCStatus struct { } // +kubebuilder:object:root=true +// +kubebuilder:resource:path=linodevpcs,scope=Namespaced,categories=cluster-api,shortName=lvpc // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="VPC is ready" // +kubebuilder:metadata:labels="clusterctl.cluster.x-k8s.io/move-hierarchy=true" // LinodeVPC is the Schema for the linodemachines API diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4c32c3534..808460474 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -370,6 +370,11 @@ func (in *LinodeMachineSpec) DeepCopyInto(out *LinodeMachineSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PrivateIP != nil { + in, out := &in.PrivateIP, &out.PrivateIP + *out = new(bool) + **out = **in + } if in.Tags != nil { in, out := &in.Tags, &out.Tags *out = make([]string, len(*in)) diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index 37ee24b38..83e059fd0 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -23,8 +23,7 @@ type MachineScopeParams struct { } type MachineScope struct { - client k8sClient - + Client k8sClient PatchHelper *patch.Helper Cluster *clusterv1.Cluster Machine *clusterv1.Machine @@ -93,7 +92,7 @@ func NewMachineScope(ctx context.Context, apiKey string, params MachineScopePara } return &MachineScope{ - client: params.Client, + Client: params.Client, PatchHelper: helper, Cluster: params.Cluster, Machine: params.Machine, @@ -135,7 +134,7 @@ func (m *MachineScope) GetBootstrapData(ctx context.Context) ([]byte, error) { secret := &corev1.Secret{} key := types.NamespacedName{Namespace: m.LinodeMachine.Namespace, Name: *m.Machine.Spec.Bootstrap.DataSecretName} - if err := m.client.Get(ctx, key, secret); err != nil { + if err := m.Client.Get(ctx, key, secret); err != nil { return []byte{}, fmt.Errorf( "failed to retrieve bootstrap data secret for LinodeMachine %s/%s", m.LinodeMachine.Namespace, diff --git a/cloud/scope/machine_test.go b/cloud/scope/machine_test.go index 5705c6ef8..a355a69d8 100644 --- a/cloud/scope/machine_test.go +++ b/cloud/scope/machine_test.go @@ -543,7 +543,7 @@ func TestMachineScopeGetBootstrapData(t *testing.T) { testcase.expects(mockK8sClient) mScope := &MachineScope{ - client: mockK8sClient, + Client: mockK8sClient, PatchHelper: &patch.Helper{}, // empty patch helper Cluster: testcase.fields.Cluster, Machine: testcase.fields.Machine, 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 af67096c8..b052591a2 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachines.yaml @@ -78,7 +78,7 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf - backupId: + backupID: type: integer x-kubernetes-validations: - message: Value is immutable @@ -107,7 +107,7 @@ spec: type: string type: object x-kubernetes-map-type: atomic - firewallId: + firewallID: type: integer x-kubernetes-validations: - message: Value is immutable @@ -161,7 +161,7 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf - privateIp: + privateIP: type: boolean x-kubernetes-validations: - message: Value is immutable 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 f4d5c1b95..6ea013c2e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodemachinetemplates.yaml @@ -65,7 +65,7 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf - backupId: + backupID: type: integer x-kubernetes-validations: - message: Value is immutable @@ -94,7 +94,7 @@ spec: type: string type: object x-kubernetes-map-type: atomic - firewallId: + firewallID: type: integer x-kubernetes-validations: - message: Value is immutable @@ -150,7 +150,7 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf - privateIp: + privateIP: type: boolean x-kubernetes-validations: - message: Value is immutable diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragebuckets.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragebuckets.yaml index b5daaaed1..1b2989c1a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragebuckets.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragebuckets.yaml @@ -10,6 +10,8 @@ metadata: spec: group: infrastructure.cluster.x-k8s.io names: + categories: + - cluster-api kind: LinodeObjectStorageBucket listKind: LinodeObjectStorageBucketList plural: linodeobjectstoragebuckets diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodevpcs.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodevpcs.yaml index 4d73a774b..99071013b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodevpcs.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodevpcs.yaml @@ -10,13 +10,22 @@ metadata: spec: group: infrastructure.cluster.x-k8s.io names: + categories: + - cluster-api kind: LinodeVPC listKind: LinodeVPCList plural: linodevpcs + shortNames: + - lvpc singular: linodevpc scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: VPC is ready + jsonPath: .status.ready + name: Ready + type: string + name: v1alpha1 schema: openAPIV3Schema: description: LinodeVPC is the Schema for the linodemachines API diff --git a/controller/linodemachine_controller_helpers.go b/controller/linodemachine_controller_helpers.go index 535f37cfb..ee51c770d 100644 --- a/controller/linodemachine_controller_helpers.go +++ b/controller/linodemachine_controller_helpers.go @@ -62,51 +62,14 @@ func (r *LinodeMachineReconciler) newCreateConfig(ctx context.Context, machineSc createConfig.Booted = util.Pointer(false) - createConfig.PrivateIP = true - - bootstrapData, err := machineScope.GetBootstrapData(ctx) - if err != nil { - logger.Info("Failed to get bootstrap data", "error", err.Error()) - - return nil, err - } - if len(bootstrapData) > maxBootstrapDataBytes { - err = errors.New("bootstrap data too large") - logger.Info(fmt.Sprintf("decoded bootstrap data exceeds size limit of %d bytes", maxBootstrapDataBytes), "error", err.Error()) - + if err := setUserData(ctx, machineScope, createConfig, logger); err != nil { return nil, err } - region, err := machineScope.LinodeClient.GetRegion(ctx, machineScope.LinodeMachine.Spec.Region) - if err != nil { - return nil, err - } - regionMetadataSupport := slices.Contains(region.Capabilities, "Metadata") - image, err := machineScope.LinodeClient.GetImage(ctx, machineScope.LinodeMachine.Spec.Image) - if err != nil { - return nil, err - } - imageMetadataSupport := slices.Contains(image.Capabilities, "cloud-init") - if imageMetadataSupport && regionMetadataSupport { - createConfig.Metadata = &linodego.InstanceMetadataOptions{ - UserData: b64.StdEncoding.EncodeToString(bootstrapData), - } + if machineScope.LinodeMachine.Spec.PrivateIP != nil { + createConfig.PrivateIP = *machineScope.LinodeMachine.Spec.PrivateIP } else { - logger.Info(fmt.Sprintf("using StackScripts for bootstrapping. imageMetadataSupport: %t, regionMetadataSupport: %t", - imageMetadataSupport, regionMetadataSupport)) - capiStackScriptID, err := services.EnsureStackscript(ctx, machineScope) - if err != nil { - return nil, err - } - createConfig.StackScriptID = capiStackScriptID - // ###### WARNING, currently label, region and type are supported as cloud-init variables, any changes ###### - // any changes to this could be potentially backwards incompatible and should be noted through a backwards incompatible version update ##### - instanceData := fmt.Sprintf("label: %s\nregion: %s\ntype: %s", machineScope.LinodeMachine.Name, machineScope.LinodeMachine.Spec.Region, machineScope.LinodeMachine.Spec.Type) - // ###### WARNING ###### - createConfig.StackScriptData = map[string]string{ - "instancedata": b64.StdEncoding.EncodeToString([]byte(instanceData)), - "userdata": b64.StdEncoding.EncodeToString(bootstrapData), - } + createConfig.PrivateIP = true } if createConfig.Tags == nil { @@ -394,3 +357,54 @@ func linodeMachineSpecToInstanceCreateConfig(machineSpec infrav1alpha1.LinodeMac return &createConfig } + +func setUserData(ctx context.Context, machineScope *scope.MachineScope, createConfig *linodego.InstanceCreateOptions, logger logr.Logger) error { + bootstrapData, err := machineScope.GetBootstrapData(ctx) + if err != nil { + logger.Error(err, "Failed to get bootstrap data") + + return err + } + if len(bootstrapData) > maxBootstrapDataBytes { + err = errors.New("bootstrap data too large") + logger.Error(err, "decoded bootstrap data exceeds size limit", + "limit", maxBootstrapDataBytes, + ) + + return err + } + + region, err := machineScope.LinodeClient.GetRegion(ctx, machineScope.LinodeMachine.Spec.Region) + if err != nil { + return fmt.Errorf("get region: %w", err) + } + regionMetadataSupport := slices.Contains(region.Capabilities, "Metadata") + image, err := machineScope.LinodeClient.GetImage(ctx, machineScope.LinodeMachine.Spec.Image) + if err != nil { + return fmt.Errorf("get image: %w", err) + } + imageMetadataSupport := slices.Contains(image.Capabilities, "cloud-init") + if imageMetadataSupport && regionMetadataSupport { + createConfig.Metadata = &linodego.InstanceMetadataOptions{ + UserData: b64.StdEncoding.EncodeToString(bootstrapData), + } + } else { + logger.Info("using StackScripts for bootstrapping", + "imageMetadataSupport", imageMetadataSupport, + "regionMetadataSupport", regionMetadataSupport, + ) + capiStackScriptID, err := services.EnsureStackscript(ctx, machineScope) + if err != nil { + return fmt.Errorf("ensure stackscript: %w", err) + } + createConfig.StackScriptID = capiStackScriptID + // WARNING: label, region and type are currently supported as cloud-init variables, + // any changes to this could be potentially backwards incompatible and should be noted through a backwards incompatible version update + instanceData := fmt.Sprintf("label: %s\nregion: %s\ntype: %s", machineScope.LinodeMachine.Name, machineScope.LinodeMachine.Spec.Region, machineScope.LinodeMachine.Spec.Type) + createConfig.StackScriptData = map[string]string{ + "instancedata": b64.StdEncoding.EncodeToString([]byte(instanceData)), + "userdata": b64.StdEncoding.EncodeToString(bootstrapData), + } + } + return nil +} diff --git a/controller/linodemachine_controller_helpers_test.go b/controller/linodemachine_controller_helpers_test.go index c9075ebab..cd43aecde 100644 --- a/controller/linodemachine_controller_helpers_test.go +++ b/controller/linodemachine_controller_helpers_test.go @@ -2,14 +2,28 @@ package controller import ( "bytes" + "context" + b64 "encoding/base64" "encoding/gob" + "fmt" "testing" + "github.com/go-logr/logr" "github.com/linode/linodego" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" infrav1alpha1 "github.com/linode/cluster-api-provider-linode/api/v1alpha1" + "github.com/linode/cluster-api-provider-linode/cloud/scope" + "github.com/linode/cluster-api-provider-linode/mock" + "github.com/linode/cluster-api-provider-linode/util" ) func TestLinodeMachineSpecToCreateInstanceConfig(t *testing.T) { @@ -41,7 +55,7 @@ func TestLinodeMachineSpecToCreateInstanceConfig(t *testing.T) { }, }, BackupsEnabled: true, - PrivateIP: true, + PrivateIP: util.Pointer(true), Tags: []string{"tag"}, FirewallID: 1, } @@ -61,3 +75,295 @@ func TestLinodeMachineSpecToCreateInstanceConfig(t *testing.T) { assert.Equal(t, machineSpec, actualMachineSpec) } + +func TestSetUserData(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + machineScope *scope.MachineScope + createConfig *linodego.InstanceCreateOptions + wantConfig *linodego.InstanceCreateOptions + expectedError error + expects func(client *mock.MockLinodeMachineClient, kClient *mock.Mockk8sClient) + }{ + { + name: "Success - SetUserData metadata", + machineScope: &scope.MachineScope{Machine: &v1beta1.Machine{ + Spec: v1beta1.MachineSpec{ + ClusterName: "", + Bootstrap: v1beta1.Bootstrap{ + DataSecretName: ptr.To("test-data"), + }, + InfrastructureRef: corev1.ObjectReference{}, + }, + }, LinodeMachine: &infrav1alpha1.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: infrav1alpha1.LinodeMachineSpec{Region: "us-ord", Image: "linode/ubuntu22.04"}, + Status: infrav1alpha1.LinodeMachineStatus{}, + }}, + createConfig: &linodego.InstanceCreateOptions{}, + wantConfig: &linodego.InstanceCreateOptions{Metadata: &linodego.InstanceMetadataOptions{ + UserData: b64.StdEncoding.EncodeToString([]byte("test-data")), + }}, + expects: func(mockClient *mock.MockLinodeMachineClient, kMock *mock.Mockk8sClient) { + kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { + cred := corev1.Secret{ + Data: map[string][]byte{ + "value": []byte("test-data"), + }, + } + *obj = cred + return nil + }) + mockClient.EXPECT().GetRegion(gomock.Any(), "us-ord").Return(&linodego.Region{ + Capabilities: []string{"Metadata"}, + }, nil) + mockClient.EXPECT().GetImage(gomock.Any(), "linode/ubuntu22.04").Return(&linodego.Image{ + Capabilities: []string{"cloud-init"}, + }, nil) + }, + }, + { + name: "Success - SetUserData StackScript", + machineScope: &scope.MachineScope{Machine: &v1beta1.Machine{ + Spec: v1beta1.MachineSpec{ + ClusterName: "", + Bootstrap: v1beta1.Bootstrap{ + DataSecretName: ptr.To("test-data"), + }, + InfrastructureRef: corev1.ObjectReference{}, + }, + }, LinodeMachine: &infrav1alpha1.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: infrav1alpha1.LinodeMachineSpec{Region: "us-east", Image: "linode/ubuntu22.04", Type: "g6-standard-1"}, + Status: infrav1alpha1.LinodeMachineStatus{}, + }}, + createConfig: &linodego.InstanceCreateOptions{}, + wantConfig: &linodego.InstanceCreateOptions{StackScriptID: 1234, StackScriptData: map[string]string{ + "instancedata": b64.StdEncoding.EncodeToString([]byte("label: test-cluster\nregion: us-east\ntype: g6-standard-1")), + "userdata": b64.StdEncoding.EncodeToString([]byte("test-data")), + }}, + expects: func(mockClient *mock.MockLinodeMachineClient, kMock *mock.Mockk8sClient) { + kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { + cred := corev1.Secret{ + Data: map[string][]byte{ + "value": []byte("test-data"), + }, + } + *obj = cred + return nil + }) + mockClient.EXPECT().GetRegion(gomock.Any(), "us-east").Return(&linodego.Region{ + Capabilities: []string{"Metadata"}, + }, nil) + mockClient.EXPECT().GetImage(gomock.Any(), "linode/ubuntu22.04").Return(&linodego.Image{}, nil) + mockClient.EXPECT().ListStackscripts(gomock.Any(), &linodego.ListOptions{Filter: "{\"label\":\"CAPL-dev\"}"}).Return([]linodego.Stackscript{{ + Label: "CAPI Test 1", + ID: 1234, + }}, nil) + }, + }, + { + name: "Error - SetUserData large bootstrap data", + machineScope: &scope.MachineScope{Machine: &v1beta1.Machine{ + Spec: v1beta1.MachineSpec{ + ClusterName: "", + Bootstrap: v1beta1.Bootstrap{ + DataSecretName: ptr.To("test-data"), + }, + InfrastructureRef: corev1.ObjectReference{}, + }, + }, LinodeMachine: &infrav1alpha1.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: infrav1alpha1.LinodeMachineSpec{Region: "us-ord", Image: "linode/ubuntu22.04"}, + Status: infrav1alpha1.LinodeMachineStatus{}, + }}, + createConfig: &linodego.InstanceCreateOptions{}, + wantConfig: &linodego.InstanceCreateOptions{}, + expects: func(mockClient *mock.MockLinodeMachineClient, kMock *mock.Mockk8sClient) { + kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { + cred := corev1.Secret{ + Data: map[string][]byte{ + "value": make([]byte, maxBootstrapDataBytes+1), + }, + } + *obj = cred + return nil + }) + }, + expectedError: fmt.Errorf("bootstrap data too large"), + }, + { + name: "Error - SetUserData get bootstrap data", + machineScope: &scope.MachineScope{Machine: &v1beta1.Machine{ + Spec: v1beta1.MachineSpec{ + ClusterName: "", + Bootstrap: v1beta1.Bootstrap{ + ConfigRef: nil, + DataSecretName: nil, + }, + InfrastructureRef: corev1.ObjectReference{}, + }, + }, LinodeMachine: &infrav1alpha1.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: infrav1alpha1.LinodeMachineSpec{}, + Status: infrav1alpha1.LinodeMachineStatus{}, + }}, + createConfig: &linodego.InstanceCreateOptions{}, + wantConfig: &linodego.InstanceCreateOptions{}, + expects: func(c *mock.MockLinodeMachineClient, k *mock.Mockk8sClient) { + }, + expectedError: fmt.Errorf("bootstrap data secret is nil for LinodeMachine default/test-cluster"), + }, + { + name: "Error - SetUserData failed to get regions", + machineScope: &scope.MachineScope{Machine: &v1beta1.Machine{ + Spec: v1beta1.MachineSpec{ + ClusterName: "", + Bootstrap: v1beta1.Bootstrap{ + DataSecretName: ptr.To("test-data"), + }, + InfrastructureRef: corev1.ObjectReference{}, + }, + }, LinodeMachine: &infrav1alpha1.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: infrav1alpha1.LinodeMachineSpec{Region: "us-ord", Image: "linode/ubuntu22.04"}, + Status: infrav1alpha1.LinodeMachineStatus{}, + }}, + createConfig: &linodego.InstanceCreateOptions{}, + wantConfig: &linodego.InstanceCreateOptions{}, + expects: func(mockClient *mock.MockLinodeMachineClient, kMock *mock.Mockk8sClient) { + kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { + cred := corev1.Secret{ + Data: map[string][]byte{ + "value": []byte("hello"), + }, + } + *obj = cred + return nil + }) + mockClient.EXPECT().GetRegion(gomock.Any(), "us-ord").Return(nil, fmt.Errorf("cannot find region")) + }, + expectedError: fmt.Errorf("cannot find region"), + }, + { + name: "Error - SetUserData failed to get images", + machineScope: &scope.MachineScope{Machine: &v1beta1.Machine{ + Spec: v1beta1.MachineSpec{ + ClusterName: "", + Bootstrap: v1beta1.Bootstrap{ + DataSecretName: ptr.To("test-data"), + }, + InfrastructureRef: corev1.ObjectReference{}, + }, + }, LinodeMachine: &infrav1alpha1.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: infrav1alpha1.LinodeMachineSpec{Region: "us-ord", Image: "linode/ubuntu22.04"}, + Status: infrav1alpha1.LinodeMachineStatus{}, + }}, + createConfig: &linodego.InstanceCreateOptions{}, + wantConfig: &linodego.InstanceCreateOptions{}, + expects: func(mockClient *mock.MockLinodeMachineClient, kMock *mock.Mockk8sClient) { + kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { + cred := corev1.Secret{ + Data: map[string][]byte{ + "value": []byte("hello"), + }, + } + *obj = cred + return nil + }) + mockClient.EXPECT().GetRegion(gomock.Any(), "us-ord").Return(&linodego.Region{ + Capabilities: []string{"Metadata"}, + }, nil) + mockClient.EXPECT().GetImage(gomock.Any(), "linode/ubuntu22.04").Return(nil, fmt.Errorf("cannot find image")) + }, + expectedError: fmt.Errorf("cannot find image"), + }, + { + name: "Error - SetUserData failed to get stackscripts", + machineScope: &scope.MachineScope{Machine: &v1beta1.Machine{ + Spec: v1beta1.MachineSpec{ + ClusterName: "", + Bootstrap: v1beta1.Bootstrap{ + DataSecretName: ptr.To("test-data"), + }, + InfrastructureRef: corev1.ObjectReference{}, + }, + }, LinodeMachine: &infrav1alpha1.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "default", + }, + Spec: infrav1alpha1.LinodeMachineSpec{Region: "us-east", Image: "linode/ubuntu22.04", Type: "g6-standard-1"}, + Status: infrav1alpha1.LinodeMachineStatus{}, + }}, + createConfig: &linodego.InstanceCreateOptions{}, + wantConfig: &linodego.InstanceCreateOptions{StackScriptID: 1234, StackScriptData: map[string]string{ + "instancedata": b64.StdEncoding.EncodeToString([]byte("label: test-cluster\nregion: us-east\ntype: g6-standard-1")), + "userdata": b64.StdEncoding.EncodeToString([]byte("test-data")), + }}, + expects: func(mockClient *mock.MockLinodeMachineClient, kMock *mock.Mockk8sClient) { + kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { + cred := corev1.Secret{ + Data: map[string][]byte{ + "value": []byte("test-data"), + }, + } + *obj = cred + return nil + }) + mockClient.EXPECT().GetRegion(gomock.Any(), "us-east").Return(&linodego.Region{ + Capabilities: []string{"Metadata"}, + }, nil) + mockClient.EXPECT().GetImage(gomock.Any(), "linode/ubuntu22.04").Return(&linodego.Image{}, nil) + mockClient.EXPECT().ListStackscripts(gomock.Any(), &linodego.ListOptions{Filter: "{\"label\":\"CAPL-dev\"}"}).Return(nil, fmt.Errorf("failed to get stackscripts")) + }, + expectedError: fmt.Errorf("ensure stackscript: failed to get stackscript with label CAPL-dev: failed to get stackscripts"), + }, + } + for _, tt := range tests { + testcase := tt + t.Run(testcase.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mock.NewMockLinodeMachineClient(ctrl) + mockK8sClient := mock.NewMockk8sClient(ctrl) + testcase.machineScope.LinodeClient = mockClient + testcase.machineScope.Client = mockK8sClient + testcase.expects(mockClient, mockK8sClient) + logger := logr.Logger{} + + err := setUserData(context.Background(), testcase.machineScope, testcase.createConfig, logger) + if testcase.expectedError != nil { + assert.ErrorContains(t, err, testcase.expectedError.Error()) + } else { + assert.Equal(t, testcase.wantConfig.Metadata, testcase.createConfig.Metadata) + assert.Equal(t, testcase.wantConfig.StackScriptID, testcase.createConfig.StackScriptID) + assert.Equal(t, testcase.wantConfig.StackScriptData, testcase.createConfig.StackScriptData) + } + }) + } +} diff --git a/devbox.json b/devbox.json index 56ebf1388..e85c3b7ab 100644 --- a/devbox.json +++ b/devbox.json @@ -17,12 +17,14 @@ "mdbook@latest", "mdbook-admonish@latest", "mockgen@latest", - "kyverno-chainsaw@latest" + "kyverno-chainsaw@latest", + "kubernetes-helm@latest", + "kubectl@latest" ], "shell": { "init_hook": [ "export \"GOROOT=$(go env GOROOT)\"" ], - "scripts": {} + "scripts": {} } } diff --git a/devbox.lock b/devbox.lock index 520bd792a..ae59887ed 100644 --- a/devbox.lock +++ b/devbox.lock @@ -209,6 +209,90 @@ } } }, + "kubectl@latest": { + "last_modified": "2024-03-22T11:26:23Z", + "resolved": "github:NixOS/nixpkgs/a3ed7406349a9335cb4c2a71369b697cecd9d351#kubectl", + "source": "devbox-search", + "version": "1.29.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/la4hrwhgy434f3y3qrapffjsc330gi9g-kubectl-1.29.3", + "default": true + }, + { + "name": "man", + "path": "/nix/store/z27px1zgfy0iyca6iiv4x7jmqw1mqmqs-kubectl-1.29.3-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/ns4wcsjkzzidiimxiv5si5mhqhxksqp7-kubectl-1.29.3-convert" + } + ], + "store_path": "/nix/store/la4hrwhgy434f3y3qrapffjsc330gi9g-kubectl-1.29.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/j138rcn5m64qd4qvnnrnk0qbkc67w95b-kubectl-1.29.3", + "default": true + }, + { + "name": "man", + "path": "/nix/store/rbcalzmyvkhgwf2zxjw6j0al1vciccrh-kubectl-1.29.3-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/acbp07cn2wy7xrsir402bqx71hrvb9w9-kubectl-1.29.3-convert" + } + ], + "store_path": "/nix/store/j138rcn5m64qd4qvnnrnk0qbkc67w95b-kubectl-1.29.3" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jclvx3km8dr03cikgm4n1rx5ai5zvy5n-kubectl-1.29.3", + "default": true + }, + { + "name": "man", + "path": "/nix/store/w0x2wbvimf9s46i9hqbh61dz0kf5a1cg-kubectl-1.29.3-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/190m16nhp9jm4rkpy15i6d9khf3ibdb9-kubectl-1.29.3-convert" + } + ], + "store_path": "/nix/store/jclvx3km8dr03cikgm4n1rx5ai5zvy5n-kubectl-1.29.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/q01ksybv8bynfk2rwjiqpbqxasjnj3rw-kubectl-1.29.3", + "default": true + }, + { + "name": "man", + "path": "/nix/store/hfv8hjg3ymrr3fv1z4ffm6kbxc7r53bl-kubectl-1.29.3-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/jwwf2lqbrm099fs5v153y9cjvba13kb0-kubectl-1.29.3-convert" + } + ], + "store_path": "/nix/store/q01ksybv8bynfk2rwjiqpbqxasjnj3rw-kubectl-1.29.3" + } + } + }, "kubernetes-controller-tools@latest": { "last_modified": "2024-03-08T13:51:52Z", "resolved": "github:NixOS/nixpkgs/a343533bccc62400e8a9560423486a3b6c11a23b#kubernetes-controller-tools", @@ -257,6 +341,54 @@ } } }, + "kubernetes-helm@latest": { + "last_modified": "2024-03-22T11:26:23Z", + "resolved": "github:NixOS/nixpkgs/a3ed7406349a9335cb4c2a71369b697cecd9d351#kubernetes-helm", + "source": "devbox-search", + "version": "3.14.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/nbz58p0ak92q3hqp39mfrvhj33svh3k4-kubernetes-helm-3.14.3", + "default": true + } + ], + "store_path": "/nix/store/nbz58p0ak92q3hqp39mfrvhj33svh3k4-kubernetes-helm-3.14.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/1f9184lbcv55mj1sqrrf022r8ln49kpz-kubernetes-helm-3.14.3", + "default": true + } + ], + "store_path": "/nix/store/1f9184lbcv55mj1sqrrf022r8ln49kpz-kubernetes-helm-3.14.3" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/f2v22pzmbfypqlrniragqbkxmrkk3j4h-kubernetes-helm-3.14.3", + "default": true + } + ], + "store_path": "/nix/store/f2v22pzmbfypqlrniragqbkxmrkk3j4h-kubernetes-helm-3.14.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0vf3gsyjrkxrkwygi3igidd4hzab8iq9-kubernetes-helm-3.14.3", + "default": true + } + ], + "store_path": "/nix/store/0vf3gsyjrkxrkwygi3igidd4hzab8iq9-kubernetes-helm-3.14.3" + } + } + }, "kustomize@latest": { "last_modified": "2024-02-26T19:46:43Z", "resolved": "github:NixOS/nixpkgs/548a86b335d7ecd8b57ec617781f5e652ab0c38e#kustomize",