From e3566e41f8e01eeea8821c5200ce47efc8b9b6fa Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 3 Apr 2024 17:00:29 -0400 Subject: [PATCH] add support for stackscript based bootstrapping (#211) * add support for stackscript based bootstrapping --- Makefile | 2 +- api/v1alpha1/linodemachine_types.go | 7 - api/v1alpha1/zz_generated.deepcopy.go | 12 -- cloud/scope/client.go | 4 + cloud/services/stackscript.sh | 21 +++ cloud/services/stackscripts.go | 50 ++++++++ cloud/services/stackscripts_test.go | 115 +++++++++++++++++ ...cture.cluster.x-k8s.io_linodemachines.yaml | 22 ---- ...uster.x-k8s.io_linodemachinetemplates.yaml | 22 ---- .../linodemachine_controller_helpers.go | 35 ++++- .../linodemachine_controller_helpers_test.go | 7 +- devbox.lock | 42 +++++- docs/src/topics/getting-started.md | 4 + mock/client.go | 120 ++++++++++++++++++ util/filter.go | 11 +- util/filter_test.go | 9 ++ 16 files changed, 400 insertions(+), 83 deletions(-) create mode 100644 cloud/services/stackscript.sh create mode 100644 cloud/services/stackscripts.go create mode 100644 cloud/services/stackscripts_test.go diff --git a/Makefile b/Makefile index 33b56b00..d23f49cb 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,7 @@ gosec: ## Run gosec against code. .PHONY: lint lint: ## Run lint against code. - docker run --rm -w /workdir -v $(PWD):/workdir golangci/golangci-lint:v1.56.1 golangci-lint run -c .golangci.yml + docker run --rm -w /workdir -v $(PWD):/workdir golangci/golangci-lint:v1.57.2 golangci-lint run -c .golangci.yml .PHONY: nilcheck nilcheck: nilaway ## Run nil check against code. diff --git a/api/v1alpha1/linodemachine_types.go b/api/v1alpha1/linodemachine_types.go index 8b632ced..72e5e5ee 100644 --- a/api/v1alpha1/linodemachine_types.go +++ b/api/v1alpha1/linodemachine_types.go @@ -51,10 +51,6 @@ 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" - StackScriptID int `json:"stackscriptId,omitempty"` - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" - StackScriptData map[string]string `json:"stackscriptData,omitempty"` - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" BackupID int `json:"backupId,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" Image string `json:"image,omitempty"` @@ -67,9 +63,6 @@ type LinodeMachineSpec struct { // +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" - // +optional - Metadata *InstanceMetadataOptions `json:"metadata,omitempty"` - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" FirewallID int `json:"firewallId,omitempty"` // CredentialsRef is a reference to a Secret that contains the credentials diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 25999e53..4c32c353 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -363,13 +363,6 @@ func (in *LinodeMachineSpec) DeepCopyInto(out *LinodeMachineSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - if in.StackScriptData != nil { - in, out := &in.StackScriptData, &out.StackScriptData - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } if in.Interfaces != nil { in, out := &in.Interfaces, &out.Interfaces *out = make([]InstanceConfigInterfaceCreateOptions, len(*in)) @@ -382,11 +375,6 @@ func (in *LinodeMachineSpec) DeepCopyInto(out *LinodeMachineSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - if in.Metadata != nil { - in, out := &in.Metadata, &out.Metadata - *out = new(InstanceMetadataOptions) - **out = **in - } if in.CredentialsRef != nil { in, out := &in.CredentialsRef, &out.CredentialsRef *out = new(v1.SecretReference) diff --git a/cloud/scope/client.go b/cloud/scope/client.go index 6201247d..ff030aee 100644 --- a/cloud/scope/client.go +++ b/cloud/scope/client.go @@ -28,6 +28,10 @@ type LinodeInstanceClient interface { CreateInstanceDisk(ctx context.Context, linodeID int, opts linodego.InstanceDiskCreateOptions) (*linodego.InstanceDisk, error) GetInstance(ctx context.Context, linodeID int) (*linodego.Instance, error) DeleteInstance(ctx context.Context, linodeID int) error + GetRegion(ctx context.Context, regionID string) (*linodego.Region, error) + GetImage(ctx context.Context, imageID string) (*linodego.Image, error) + CreateStackscript(ctx context.Context, opts linodego.StackscriptCreateOptions) (*linodego.Stackscript, error) + ListStackscripts(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Stackscript, error) WaitForInstanceDiskStatus(ctx context.Context, instanceID int, diskID int, status linodego.DiskStatus, timeoutSeconds int) (*linodego.InstanceDisk, error) } diff --git a/cloud/services/stackscript.sh b/cloud/services/stackscript.sh new file mode 100644 index 00000000..9d0ab9cd --- /dev/null +++ b/cloud/services/stackscript.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# +# + +cat > /etc/cloud/cloud.cfg.d/100_none.cfg < +# + +cat > /etc/cloud/cloud.cfg.d/100_none.cfg < export LINODE_CONTROL_PLANE_MACHINE_TYPE=g6-standard-2 export LINODE_MACHINE_TYPE=g6-standard-2 ``` +```admonish warning +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 +``` ## Register linode locally as an infrastructure provider 1. Generate local release files diff --git a/mock/client.go b/mock/client.go index ca767a10..c3f40ec3 100644 --- a/mock/client.go +++ b/mock/client.go @@ -133,6 +133,21 @@ func (mr *MockLinodeMachineClientMockRecorder) CreateNodeBalancerNode(ctx, nodeb return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNodeBalancerNode", reflect.TypeOf((*MockLinodeMachineClient)(nil).CreateNodeBalancerNode), ctx, nodebalancerID, configID, opts) } +// CreateStackscript mocks base method. +func (m *MockLinodeMachineClient) CreateStackscript(ctx context.Context, opts linodego.StackscriptCreateOptions) (*linodego.Stackscript, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateStackscript", ctx, opts) + ret0, _ := ret[0].(*linodego.Stackscript) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateStackscript indicates an expected call of CreateStackscript. +func (mr *MockLinodeMachineClientMockRecorder) CreateStackscript(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStackscript", reflect.TypeOf((*MockLinodeMachineClient)(nil).CreateStackscript), ctx, opts) +} + // DeleteInstance mocks base method. func (m *MockLinodeMachineClient) DeleteInstance(ctx context.Context, linodeID int) error { m.ctrl.T.Helper() @@ -175,6 +190,21 @@ func (mr *MockLinodeMachineClientMockRecorder) DeleteNodeBalancerNode(ctx, nodeb return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodeBalancerNode", reflect.TypeOf((*MockLinodeMachineClient)(nil).DeleteNodeBalancerNode), ctx, nodebalancerID, configID, nodeID) } +// GetImage mocks base method. +func (m *MockLinodeMachineClient) GetImage(ctx context.Context, imageID string) (*linodego.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImage", ctx, imageID) + ret0, _ := ret[0].(*linodego.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetImage indicates an expected call of GetImage. +func (mr *MockLinodeMachineClientMockRecorder) GetImage(ctx, imageID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImage", reflect.TypeOf((*MockLinodeMachineClient)(nil).GetImage), ctx, imageID) +} + // GetInstance mocks base method. func (m *MockLinodeMachineClient) GetInstance(ctx context.Context, linodeID int) (*linodego.Instance, error) { m.ctrl.T.Helper() @@ -220,6 +250,21 @@ func (mr *MockLinodeMachineClientMockRecorder) GetInstanceIPAddresses(ctx, linod return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceIPAddresses", reflect.TypeOf((*MockLinodeMachineClient)(nil).GetInstanceIPAddresses), ctx, linodeID) } +// GetRegion mocks base method. +func (m *MockLinodeMachineClient) GetRegion(ctx context.Context, regionID string) (*linodego.Region, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegion", ctx, regionID) + ret0, _ := ret[0].(*linodego.Region) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRegion indicates an expected call of GetRegion. +func (mr *MockLinodeMachineClientMockRecorder) GetRegion(ctx, regionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegion", reflect.TypeOf((*MockLinodeMachineClient)(nil).GetRegion), ctx, regionID) +} + // GetVPC mocks base method. func (m *MockLinodeMachineClient) GetVPC(ctx context.Context, vpcID int) (*linodego.VPC, error) { m.ctrl.T.Helper() @@ -280,6 +325,21 @@ func (mr *MockLinodeMachineClientMockRecorder) ListNodeBalancers(ctx, opts any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNodeBalancers", reflect.TypeOf((*MockLinodeMachineClient)(nil).ListNodeBalancers), ctx, opts) } +// ListStackscripts mocks base method. +func (m *MockLinodeMachineClient) ListStackscripts(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Stackscript, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListStackscripts", ctx, opts) + ret0, _ := ret[0].([]linodego.Stackscript) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListStackscripts indicates an expected call of ListStackscripts. +func (mr *MockLinodeMachineClientMockRecorder) ListStackscripts(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStackscripts", reflect.TypeOf((*MockLinodeMachineClient)(nil).ListStackscripts), ctx, opts) +} + // ResizeInstanceDisk mocks base method. func (m *MockLinodeMachineClient) ResizeInstanceDisk(ctx context.Context, linodeID, diskID, size int) error { m.ctrl.T.Helper() @@ -376,6 +436,21 @@ func (mr *MockLinodeInstanceClientMockRecorder) CreateInstanceDisk(ctx, linodeID return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateInstanceDisk", reflect.TypeOf((*MockLinodeInstanceClient)(nil).CreateInstanceDisk), ctx, linodeID, opts) } +// CreateStackscript mocks base method. +func (m *MockLinodeInstanceClient) CreateStackscript(ctx context.Context, opts linodego.StackscriptCreateOptions) (*linodego.Stackscript, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateStackscript", ctx, opts) + ret0, _ := ret[0].(*linodego.Stackscript) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateStackscript indicates an expected call of CreateStackscript. +func (mr *MockLinodeInstanceClientMockRecorder) CreateStackscript(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStackscript", reflect.TypeOf((*MockLinodeInstanceClient)(nil).CreateStackscript), ctx, opts) +} + // DeleteInstance mocks base method. func (m *MockLinodeInstanceClient) DeleteInstance(ctx context.Context, linodeID int) error { m.ctrl.T.Helper() @@ -390,6 +465,21 @@ func (mr *MockLinodeInstanceClientMockRecorder) DeleteInstance(ctx, linodeID any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteInstance", reflect.TypeOf((*MockLinodeInstanceClient)(nil).DeleteInstance), ctx, linodeID) } +// GetImage mocks base method. +func (m *MockLinodeInstanceClient) GetImage(ctx context.Context, imageID string) (*linodego.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImage", ctx, imageID) + ret0, _ := ret[0].(*linodego.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetImage indicates an expected call of GetImage. +func (mr *MockLinodeInstanceClientMockRecorder) GetImage(ctx, imageID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImage", reflect.TypeOf((*MockLinodeInstanceClient)(nil).GetImage), ctx, imageID) +} + // GetInstance mocks base method. func (m *MockLinodeInstanceClient) GetInstance(ctx context.Context, linodeID int) (*linodego.Instance, error) { m.ctrl.T.Helper() @@ -435,6 +525,21 @@ func (mr *MockLinodeInstanceClientMockRecorder) GetInstanceIPAddresses(ctx, lino return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceIPAddresses", reflect.TypeOf((*MockLinodeInstanceClient)(nil).GetInstanceIPAddresses), ctx, linodeID) } +// GetRegion mocks base method. +func (m *MockLinodeInstanceClient) GetRegion(ctx context.Context, regionID string) (*linodego.Region, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegion", ctx, regionID) + ret0, _ := ret[0].(*linodego.Region) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRegion indicates an expected call of GetRegion. +func (mr *MockLinodeInstanceClientMockRecorder) GetRegion(ctx, regionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegion", reflect.TypeOf((*MockLinodeInstanceClient)(nil).GetRegion), ctx, regionID) +} + // ListInstanceConfigs mocks base method. func (m *MockLinodeInstanceClient) ListInstanceConfigs(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.InstanceConfig, error) { m.ctrl.T.Helper() @@ -465,6 +570,21 @@ func (mr *MockLinodeInstanceClientMockRecorder) ListInstances(ctx, opts any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstances", reflect.TypeOf((*MockLinodeInstanceClient)(nil).ListInstances), ctx, opts) } +// ListStackscripts mocks base method. +func (m *MockLinodeInstanceClient) ListStackscripts(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Stackscript, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListStackscripts", ctx, opts) + ret0, _ := ret[0].([]linodego.Stackscript) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListStackscripts indicates an expected call of ListStackscripts. +func (mr *MockLinodeInstanceClientMockRecorder) ListStackscripts(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStackscripts", reflect.TypeOf((*MockLinodeInstanceClient)(nil).ListStackscripts), ctx, opts) +} + // ResizeInstanceDisk mocks base method. func (m *MockLinodeInstanceClient) ResizeInstanceDisk(ctx context.Context, linodeID, diskID, size int) error { m.ctrl.T.Helper() diff --git a/util/filter.go b/util/filter.go index 77aa972b..4c88e376 100644 --- a/util/filter.go +++ b/util/filter.go @@ -2,6 +2,7 @@ package util import ( "encoding/json" + "maps" "strconv" "strings" ) @@ -11,16 +12,17 @@ import ( // The fields within Filter are prioritized so that only the most-specific // field is present when Filter is marshaled to JSON. type Filter struct { - ID *int // Filter on the resource's ID (most specific). - Label string // Filter on the resource's label. - Tags []string // Filter resources by their tags (least specific). + ID *int // Filter on the resource's ID (most specific). + Label string // Filter on the resource's label. + Tags []string // Filter resources by their tags (least specific). + AdditionalFilters map[string]string // Filter resources by additional parameters } // MarshalJSON returns a JSON-encoded representation of a [Filter]. // The resulting encoded value will have exactly 1 (one) field present. // See [Filter] for details on the value precedence. func (f Filter) MarshalJSON() ([]byte, error) { - filter := make(map[string]string, 1) + filter := make(map[string]string, len(f.AdditionalFilters)+1) switch { case f.ID != nil: filter["id"] = strconv.Itoa(*f.ID) @@ -30,6 +32,7 @@ func (f Filter) MarshalJSON() ([]byte, error) { filter["tags"] = strings.Join(f.Tags, ",") } + maps.Copy(filter, f.AdditionalFilters) return json.Marshal(filter) } diff --git a/util/filter_test.go b/util/filter_test.go index c2d03c29..4af6a17b 100644 --- a/util/filter_test.go +++ b/util/filter_test.go @@ -32,6 +32,15 @@ func TestString(t *testing.T) { Tags: []string{"testtag"}, }, expectErr: false, + }, { + name: "success additional info", + filter: Filter{ + ID: nil, + Label: "", + Tags: []string{"testtag"}, + AdditionalFilters: map[string]string{"mine": "true"}, + }, + expectErr: false, }, { name: "failure unmarshal", filter: Filter{