diff --git a/.github/workflows/build_test_ci.yml b/.github/workflows/build_test_ci.yml index 2f3046410..9fe21f06f 100644 --- a/.github/workflows/build_test_ci.yml +++ b/.github/workflows/build_test_ci.yml @@ -99,6 +99,7 @@ jobs: disable-sudo: true egress-policy: block allowed-endpoints: > + api.linode.com:443 api.github.com:443 github.com:443 gcr.io:443 @@ -132,6 +133,8 @@ jobs: - name: E2E test run: make e2etest + env: + LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} - uses: actions/upload-artifact@v4 if: ${{ always() }} diff --git a/Makefile b/Makefile index 43946e6cc..bab41c2c0 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ test: manifests generate fmt vet envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -race -timeout 60s ./... -coverprofile cover.out .PHONY: e2etest -e2etest: kind kuttl kustomize clusterctl manifests generate docker-build +e2etest: kind kuttl kustomize clusterctl envsubst manifests generate docker-build @echo -n "LINODE_TOKEN=$(LINODE_TOKEN)" > config/default/.env.linode @$(CONTAINER_TOOL) tag ${IMG} capli-controller:e2e IMG=capli-controller:e2e $(KUTTL) test --config e2e/kuttl-config.yaml @@ -182,6 +182,7 @@ TILT ?= $(LOCALBIN)/tilt KIND ?= $(LOCALBIN)/kind KUTTL ?= $(LOCALBIN)/kuttl ENVTEST ?= $(LOCALBIN)/setup-envtest +ENVSUBST ?= $(LOCALBIN)/envsubst HUSKY ?= $(LOCALBIN)/husky NILAWAY ?= $(LOCALBIN)/nilaway GOVULNC ?= $(LOCALBIN)/govulncheck @@ -194,6 +195,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.13.0 TILT_VERSION ?= 0.33.6 KIND_VERSION ?= 0.20.0 KUTTL_VERSION ?= 0.15.0 +ENVSUBST_VERSION ?= v1.4.2 HUSKY_VERSION ?= v0.2.16 NILAWAY_VERSION ?= latest GOVULNC_VERSION ?= v1.0.1 @@ -253,6 +255,12 @@ envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. $(ENVTEST): $(LOCALBIN) test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest +.PHONY: envsubst +envsubst: $(ENVSUBST) ## Download envsubst locally if necessary. If wrong version is installed, it will be overwritten. +$(ENVSUBST): $(LOCALBIN) + test -s $(ENVSUBST) || \ + (cd $(LOCALBIN); curl -Lso ./envsubst https://github.com/a8m/envsubst/releases/download/$(ENVSUBST_VERSION)/envsubst-$(shell uname -s)-$(ARCH) && chmod +x envsubst) + .PHONY: husky husky: $(HUSKY) ## Download husky locally if necessary. @echo Execute install command to enable git hooks: ./bin/husky install diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 8fa75b04c..ab0520063 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -77,6 +77,8 @@ spec: secretKeyRef: name: token key: LINODE_TOKEN + - name: LINODE_API_VERSION + value: v4beta name: manager securityContext: allowPrivilegeEscalation: false diff --git a/controller/linodemachine_controller.go b/controller/linodemachine_controller.go index 6b5237282..7971004f9 100644 --- a/controller/linodemachine_controller.go +++ b/controller/linodemachine_controller.go @@ -24,6 +24,7 @@ import ( "time" "github.com/go-logr/logr" + "github.com/google/uuid" "github.com/linode/cluster-api-provider-linode/cloud/scope" "github.com/linode/cluster-api-provider-linode/util" "github.com/linode/cluster-api-provider-linode/util/reconciler" @@ -93,6 +94,8 @@ type LinodeMachineReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.0/pkg/reconcile +// +//nolint:gocyclo,cyclop // As simple as possible. func (r *LinodeMachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultedLoopTimeout(r.ReconcileTimeout)) defer cancel() @@ -101,17 +104,21 @@ func (r *LinodeMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques linodeMachine := &infrav1.LinodeMachine{} if err := r.Client.Get(ctx, req.NamespacedName, linodeMachine); err != nil { - log.Error(err, "Failed to fetch Linode machine") + if err = client.IgnoreNotFound(err); err != nil { + log.Error(err, "Failed to fetch Linode machine") + } - return ctrl.Result{}, client.IgnoreNotFound(err) + return ctrl.Result{}, err } machine, err := kutil.GetOwnerMachine(ctx, r.Client, linodeMachine.ObjectMeta) switch { case err != nil: - log.Error(err, "Failed to fetch owner machine") + if err = client.IgnoreNotFound(err); err != nil { + log.Error(err, "Failed to fetch owner machine") + } - return ctrl.Result{}, client.IgnoreNotFound(err) + return ctrl.Result{}, err case machine == nil: log.Info("Machine Controller has not yet set OwnerRef, skipping reconciliation") @@ -138,14 +145,25 @@ func (r *LinodeMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques log = log.WithValues("Linode machine: ", machine.Name) cluster, err := kutil.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta) - if err != nil { - log.Info("Failed to fetch cluster by label") + switch { + case err != nil: + if err = client.IgnoreNotFound(err); err != nil { + log.Error(err, "Failed to fetch cluster by label") + } + + return ctrl.Result{}, err + case cluster == nil: + err = errors.New("missing cluster") + + log.Error(err, "Missing cluster") - return ctrl.Result{}, client.IgnoreNotFound(err) - } else if cluster == nil { - log.Info("Failed to find cluster by label") + return ctrl.Result{}, err + case cluster.Spec.InfrastructureRef == nil: + err = errors.New("missing infrastructure reference") - return ctrl.Result{}, client.IgnoreNotFound(err) + log.Error(err, "Missing infrastructure reference") + + return ctrl.Result{}, err } linodeCluster := &infrav1.LinodeCluster{} @@ -155,9 +173,11 @@ func (r *LinodeMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques } if err = r.Client.Get(ctx, linodeClusterKey, linodeCluster); err != nil { - log.Error(err, "Failed to fetch Linode cluster") + if err = client.IgnoreNotFound(err); err != nil { + log.Error(err, "Failed to fetch Linode cluster") + } - return ctrl.Result{}, client.IgnoreNotFound(err) + return ctrl.Result{}, err } machineScope, err := scope.NewMachineScope( @@ -202,7 +222,7 @@ func (r *LinodeMachineReconciler) reconcile( r.Recorder.Event(machineScope.LinodeMachine, corev1.EventTypeWarning, string(failureReason), err.Error()) } - if patchErr := machineScope.PatchHelper.Patch(ctx, machineScope.LinodeMachine); patchErr != nil && client.IgnoreNotFound(patchErr) != nil { + if patchErr := machineScope.PatchHelper.Patch(ctx, machineScope.LinodeMachine); patchErr != nil && util.IgnoreKubeNotFound(patchErr) != nil { logger.Error(patchErr, "failed to patch LinodeMachine") err = errors.Join(err, patchErr) @@ -274,12 +294,24 @@ func (r *LinodeMachineReconciler) reconcileCreate(ctx context.Context, machineSc return nil, err } - createConfig.Tags = tags + + if createConfig.Tags == nil { + createConfig.Tags = []string{} + } + createConfig.Tags = append(createConfig.Tags, tags...) if createConfig.Label == "" { createConfig.Label = util.RenderObjectLabel(machineScope.LinodeMachine.UID) } + if createConfig.Image == "" { + createConfig.Image = reconciler.DefaultMachineControllerLinodeImage + } + + if createConfig.RootPass == "" { + createConfig.RootPass = uuid.NewString() + } + if machineScope.LinodeCluster.Spec.VPCRef != nil { iface, err := r.getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger) if err != nil { diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 000000000..0c6553f63 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1 @@ +*-generated.yaml diff --git a/e2e/Makefile b/e2e/Makefile new file mode 100644 index 000000000..6724d2e09 --- /dev/null +++ b/e2e/Makefile @@ -0,0 +1,18 @@ +MAKEFLAGS += -s + +runTestCase: + @T="$$(kuttl test --skip-delete --namespace $(NAMESPACE) $$(make _renderTestCase))" ;\ + echo "$$T" | grep -s '0-step' ;\ + echo "$$T" | grep -e '^PASS' + +_renderTestCase: + @D=$$(mktemp -d) ;\ + mkdir -p $$D/step ;\ + envsubst -i $(TPL) -o $$D/step/00-step.yaml ;\ + echo -n $$D + +getKubeUid: + @kubectl get -o jsonpath='{.metadata.uid}' -n $(NAMESPACE) $R + +callLinodeApiGet: + @curl -s -H "Authorization: Bearer $(LINODE_TOKEN)" -H "X-Filter: $(F)" "https://api.linode.com/v4beta/$(U)" diff --git a/e2e/basic/cluster/00-assert.yaml b/e2e/basic/cluster/00-assert.yaml index e69de29bb..4107fd8f0 100644 --- a/e2e/basic/cluster/00-assert.yaml +++ b/e2e/basic/cluster/00-assert.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-controller-manager + namespace: capi-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-api-provider-linode-controller-manager + namespace: cluster-api-provider-linode-system +status: + availableReplicas: 1 diff --git a/e2e/basic/machine/00-assert.yaml b/e2e/basic/machine/00-assert.yaml index e69de29bb..4107fd8f0 100644 --- a/e2e/basic/machine/00-assert.yaml +++ b/e2e/basic/machine/00-assert.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-controller-manager + namespace: capi-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-api-provider-linode-controller-manager + namespace: cluster-api-provider-linode-system +status: + availableReplicas: 1 diff --git a/e2e/basic/machine/01-assert.yaml b/e2e/basic/machine/01-assert.yaml new file mode 100644 index 000000000..ca3986171 --- /dev/null +++ b/e2e/basic/machine/01-assert.yaml @@ -0,0 +1,17 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + annotations: + cluster.x-k8s.io/paused: "true" + name: cluster-sample +spec: + paused: true +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Machine +metadata: + annotations: + cluster.x-k8s.io/paused: "true" + name: machine-sample +spec: + clusterName: cluster-sample diff --git a/e2e/basic/machine/01-create-cluster.yaml b/e2e/basic/machine/01-create-cluster.yaml new file mode 100644 index 000000000..716691a5b --- /dev/null +++ b/e2e/basic/machine/01-create-cluster.yaml @@ -0,0 +1,31 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 +kind: LinodeCluster +metadata: + annotations: + cluster.x-k8s.io/paused: "true" + name: linodecluster-sample +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + annotations: + cluster.x-k8s.io/paused: "true" + name: cluster-sample +spec: + paused: true + infrastructureRef: + name: linodecluster-sample +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Machine +metadata: + annotations: + cluster.x-k8s.io/paused: "true" + name: machine-sample +spec: + clusterName: cluster-sample + bootstrap: + configRef: + apiVersion: v1 + kind: "ConfigMap" + name: "boostrap-sample" diff --git a/e2e/basic/machine/02-assert.yaml b/e2e/basic/machine/02-assert.yaml new file mode 100644 index 000000000..dd5062815 --- /dev/null +++ b/e2e/basic/machine/02-assert.yaml @@ -0,0 +1,10 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 +kind: LinodeMachine +metadata: + name: linodemachine-sample +spec: + region: us-southeast + type: g5-nanode-1 +status: + ready: true + instanceState: running diff --git a/e2e/basic/machine/02-create-linodemachine.tpl.yml b/e2e/basic/machine/02-create-linodemachine.tpl.yml new file mode 100644 index 000000000..dac92db51 --- /dev/null +++ b/e2e/basic/machine/02-create-linodemachine.tpl.yml @@ -0,0 +1,13 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 +kind: LinodeMachine +metadata: + ownerReferences: + - apiVersion: cluster.x-k8s.io/v1beta1 + kind: Machine + name: machine-sample + uid: ${MACHINE_UID} + name: linodemachine-sample + namespace: ${NAMESPACE} +spec: + region: us-southeast + type: g5-nanode-1 diff --git a/e2e/basic/machine/02-create-linodemachine.yaml b/e2e/basic/machine/02-create-linodemachine.yaml new file mode 100644 index 000000000..825c316ca --- /dev/null +++ b/e2e/basic/machine/02-create-linodemachine.yaml @@ -0,0 +1,8 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: |- + NAMESPACE=$NAMESPACE \ + MACHINE_UID=$(R=machines/machine-sample make -C ../.. getKubeUid) \ + TPL=$PWD/02-create-linodemachine.tpl.yml \ + make -C ../.. runTestCase diff --git a/e2e/basic/machine/03-delete-linodemachine.yaml b/e2e/basic/machine/03-delete-linodemachine.yaml new file mode 100644 index 000000000..3bf5e64ad --- /dev/null +++ b/e2e/basic/machine/03-delete-linodemachine.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: LinodeMachine + name: linodemachine-sample diff --git a/e2e/basic/machine/03-error.yaml b/e2e/basic/machine/03-error.yaml new file mode 100644 index 000000000..4ff1b308e --- /dev/null +++ b/e2e/basic/machine/03-error.yaml @@ -0,0 +1,4 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 +kind: LinodeMachine +metadata: + name: linodemachine-sample diff --git a/e2e/basic/machine/04-verify-linode-instance.yaml b/e2e/basic/machine/04-verify-linode-instance.yaml new file mode 100644 index 000000000..daff3e8f9 --- /dev/null +++ b/e2e/basic/machine/04-verify-linode-instance.yaml @@ -0,0 +1,5 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: |- + U="linode/instances" F="{\\\"tags\\\":\\\"$(R=linodemachines/linodemachine-sample make -C ../.. getKubeUid)\\\"}" make -C ../.. callLinodeApiGet | grep 'results": 0' diff --git a/e2e/basic/vpc/00-assert.yaml b/e2e/basic/vpc/00-assert.yaml index e69de29bb..4107fd8f0 100644 --- a/e2e/basic/vpc/00-assert.yaml +++ b/e2e/basic/vpc/00-assert.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-controller-manager + namespace: capi-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-api-provider-linode-controller-manager + namespace: cluster-api-provider-linode-system +status: + availableReplicas: 1 diff --git a/e2e/kuttl-config.yaml b/e2e/kuttl-config.yaml index f9ddb49a7..31d344f3f 100644 --- a/e2e/kuttl-config.yaml +++ b/e2e/kuttl-config.yaml @@ -9,7 +9,7 @@ kindNodeCache: true kindContainers: - capli-controller:e2e artifactsDir: .kind -timeout: 120 +timeout: 300 commands: - command: clusterctl init - command: make deploy diff --git a/go.mod b/go.mod index 0a8da1feb..b058bb314 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.21.5 require ( github.com/go-logr/logr v1.4.1 + github.com/google/uuid v1.3.1 github.com/linode/linodego v1.27.0 github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 @@ -41,7 +42,6 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.1 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/util/reconciler/defaults.go b/util/reconciler/defaults.go index 7d75f0589..038e114c2 100644 --- a/util/reconciler/defaults.go +++ b/util/reconciler/defaults.go @@ -30,6 +30,8 @@ const ( DefaultMachineControllerWaitForRunningDelay = 5 * time.Second // DefaultMachineControllerWaitForRunningTimeout is the default timeout if instance is not running. DefaultMachineControllerWaitForRunningTimeout = 20 * time.Minute + // DefaultMachineControllerLinodeImage default image. + DefaultMachineControllerLinodeImage = "linode/ubuntu22.04" // DefaultVPCControllerWaitForHasNodesDelay is the default requeue delay if VPC has nodes. DefaultVPCControllerWaitForHasNodesDelay = 5 * time.Second