From 2ede89b4cf24833cd18b9a96079a474a3aaa496d Mon Sep 17 00:00:00 2001 From: Adrian Pedriza Date: Mon, 11 Nov 2024 18:01:16 +0100 Subject: [PATCH] Add envtest for testing controllers Signed-off-by: Adrian Pedriza --- cmd/main.go | 1 - go.mod | 14 +- go.sum | 2 + internal/controller/controlplane/helper.go | 18 +- .../k0s_controlplane_controller.go | 38 +- .../k0s_controlplane_controller_test.go | 1230 ++++++++++ .../controller/controlplane/suite_test.go | 37 + internal/controller/controlplane/util.go | 19 +- .../k0smotron.io/k0smotroncluster_etcd.go | 3 +- internal/test/builder/bootstrap.go | 104 + internal/test/builder/builders.go | 1989 +++++++++++++++++ internal/test/builder/controlplane.go | 152 ++ internal/test/builder/crds.go | 113 + internal/test/builder/doc.go | 24 + internal/test/builder/infrastructure.go | 288 +++ internal/test/builder/remediation.go | 32 + .../test/builder/zz_generated.deepcopy.go | 965 ++++++++ internal/test/envtest/environment.go | 269 +++ 18 files changed, 5267 insertions(+), 31 deletions(-) create mode 100644 internal/controller/controlplane/suite_test.go create mode 100644 internal/test/builder/bootstrap.go create mode 100644 internal/test/builder/builders.go create mode 100644 internal/test/builder/controlplane.go create mode 100644 internal/test/builder/crds.go create mode 100644 internal/test/builder/doc.go create mode 100644 internal/test/builder/infrastructure.go create mode 100644 internal/test/builder/remediation.go create mode 100644 internal/test/builder/zz_generated.deepcopy.go create mode 100644 internal/test/envtest/environment.go diff --git a/cmd/main.go b/cmd/main.go index 344a7047c..655915fba 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -203,7 +203,6 @@ func main() { if err = (&controlplane.K0sController{ Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), ClientSet: clientSet, RESTConfig: restConfig, }).SetupWithManager(mgr); err != nil { diff --git a/go.mod b/go.mod index 389fff540..b033f0b6a 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require ( github.com/cloudflare/cfssl v1.6.4 github.com/go-logr/logr v1.4.2 + github.com/gobuffalo/flect v1.0.2 github.com/google/uuid v1.6.0 github.com/imdario/mergo v0.3.16 github.com/k0sproject/k0s v1.27.2-0.20230504131248-94378e521a29 @@ -12,11 +13,15 @@ require ( github.com/k0sproject/version v0.6.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 + github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.28.4 + k8s.io/apiextensions-apiserver v0.28.4 k8s.io/apimachinery v0.28.4 k8s.io/client-go v0.28.4 + k8s.io/klog/v2 v2.100.1 + k8s.io/kubectl v0.28.4 k8s.io/kubernetes v1.28.4 k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 sigs.k8s.io/controller-runtime v0.16.5 @@ -25,7 +30,7 @@ require ( require ( github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver v1.5.0 + github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect @@ -56,7 +61,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/crypto v0.27.0 golang.org/x/sync v0.8.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/tools v0.24.0 gotest.tools/v3 v3.4.0 // indirect helm.sh/helm/v3 v3.11.3 // indirect k8s.io/kube-aggregator v0.27.2 // indirect @@ -90,7 +95,6 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/gobuffalo/flect v1.0.2 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -124,7 +128,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect @@ -151,6 +154,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.25.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.14.0 // indirect golang.org/x/sys v0.25.0 // indirect @@ -167,14 +171,12 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.28.4 // indirect k8s.io/apiserver v0.28.4 // indirect k8s.io/cloud-provider v0.27.1 // indirect k8s.io/cluster-bootstrap v0.28.4 // indirect k8s.io/component-base v0.28.4 // indirect k8s.io/component-helpers v0.28.4 // indirect k8s.io/controller-manager v0.28.4 // indirect - k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kms v0.28.4 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect k8s.io/kubelet v0.27.1 // indirect diff --git a/go.sum b/go.sum index 7a660023c..dc359eb64 100644 --- a/go.sum +++ b/go.sum @@ -551,6 +551,8 @@ k8s.io/kube-aggregator v0.28.4 h1:VIGTKc3cDaJ44bvj988MTapJyRPbWXXcCvlp7HVLq5Q= k8s.io/kube-aggregator v0.28.4/go.mod h1:SHehggsYGjVaE1CZTfhukAPpdhs7bflJiddLrabbQNY= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ= +k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c= k8s.io/kubelet v0.28.4 h1:Ypxy1jaFlSXFXbg/yVtFOU2ZxErBVRJfLu8+t4s7Dtw= k8s.io/kubelet v0.28.4/go.mod h1:w1wPI12liY/aeC70nqKYcNNkr6/nbyvdMB7P7wmww2o= k8s.io/kubernetes v1.28.4 h1:aRNxs5jb8FVTtlnxeA4FSDBVKuFwA8Gw40/U2zReBYA= diff --git a/internal/controller/controlplane/helper.go b/internal/controller/controlplane/helper.go index 808b2f1da..476bda7e9 100644 --- a/internal/controller/controlplane/helper.go +++ b/internal/controller/controlplane/helper.go @@ -24,12 +24,16 @@ import ( cpv1beta1 "github.com/k0sproject/k0smotron/api/controlplane/v1beta1" ) +const ( + generatedMachineRoleLabel = "cluster.x-k8s.io/generateMachine-role" +) + func (c *K0sController) createMachine(ctx context.Context, name string, cluster *clusterv1.Cluster, kcp *cpv1beta1.K0sControlPlane, infraRef corev1.ObjectReference, failureDomain *string) (*clusterv1.Machine, error) { machine, err := c.generateMachine(ctx, name, cluster, kcp, infraRef, failureDomain) if err != nil { return nil, fmt.Errorf("error generating machine: %w", err) } - _ = ctrl.SetControllerReference(kcp, machine, c.Scheme) + _ = ctrl.SetControllerReference(kcp, machine, c.Client.Scheme()) return machine, c.Client.Patch(ctx, machine, client.Apply, &client.PatchOptions{ FieldManager: "k0smotron", @@ -60,9 +64,9 @@ func (c *K0sController) generateMachine(_ context.Context, name string, cluster v := kcp.Spec.Version labels := map[string]string{ - "cluster.x-k8s.io/cluster-name": kcp.Name, - "cluster.x-k8s.io/control-plane": "true", - "cluster.x-k8s.io/generateMachine-role": "control-plane", + clusterv1.ClusterNameLabel: kcp.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", } for _, arg := range kcp.Spec.K0sConfigSpec.Args { @@ -174,7 +178,11 @@ func (c *K0sController) generateMachineFromTemplate(ctx context.Context, name st return nil, err } - _ = ctrl.SetControllerReference(kcp, unstructuredMachineTemplate, c.Scheme) + _ = ctrl.SetControllerReference(cluster, unstructuredMachineTemplate, c.Client.Scheme()) + err = c.Client.Patch(ctx, unstructuredMachineTemplate, client.Merge, &client.PatchOptions{FieldManager: "k0smotron"}) + if err != nil { + return nil, err + } template, found, err := unstructured.NestedMap(unstructuredMachineTemplate.UnstructuredContent(), "spec", "template") if !found { diff --git a/internal/controller/controlplane/k0s_controlplane_controller.go b/internal/controller/controlplane/k0s_controlplane_controller.go index 3fadf1c43..8fc176575 100644 --- a/internal/controller/controlplane/k0s_controlplane_controller.go +++ b/internal/controller/controlplane/k0s_controlplane_controller.go @@ -31,7 +31,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -59,15 +58,20 @@ const ( ) var ( - ErrNotReady = fmt.Errorf("waiting for the state") - ErrNewMachinesNotReady = fmt.Errorf("waiting for new machines: %w", ErrNotReady) + ErrNotReady = fmt.Errorf("waiting for the state") + ErrNewMachinesNotReady = fmt.Errorf("waiting for new machines: %w", ErrNotReady) + FRPTokenNameTemplate = "%s-frp-token" + FRPConfigMapNameTemplate = "%s-frps-config" + FRPDeploymentNameTemplate = "%s-frps" + FRPServiceNameTemplate = "%s-frps" ) type K0sController struct { client.Client - Scheme *runtime.Scheme ClientSet *kubernetes.Clientset RESTConfig *rest.Config + // workloadClusterKubeClient is used during testing to inject a fake client + workloadClusterKubeClient *kubernetes.Clientset } // +kubebuilder:rbac:groups=controlplane.cluster.x-k8s.io,resources=k0scontrolplanes/status,verbs=get;list;watch;create;update;patch;delete @@ -120,6 +124,11 @@ func (c *K0sController) Reconcile(ctx context.Context, req ctrl.Request) (res ct return ctrl.Result{}, nil } + if annotations.IsPaused(cluster, kcp) { + log.Info("Reconciliation is paused for this object or owning cluster") + return ctrl.Result{}, nil + } + // Always patch the object to update the status defer func() { log.Info("Updating status") @@ -151,11 +160,6 @@ func (c *K0sController) Reconcile(ctx context.Context, req ctrl.Request) (res ct log = log.WithValues("cluster", cluster.Name) - if annotations.IsPaused(cluster, kcp) { - log.Info("Reconciliation is paused for this object or owning cluster") - return ctrl.Result{}, nil - } - if err := c.ensureCertificates(ctx, cluster, kcp); err != nil { log.Error(err, "Failed to ensure certificates") return ctrl.Result{}, err @@ -615,7 +619,7 @@ token = ` + frpToken + ` ` } - frpsCMName := kcp.GetName() + "-frps-config" + frpsCMName := fmt.Sprintf(FRPConfigMapNameTemplate, kcp.GetName()) cm := corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", @@ -630,7 +634,7 @@ token = ` + frpToken + ` }, } - _ = ctrl.SetControllerReference(kcp, &cm, c.Scheme) + _ = ctrl.SetControllerReference(kcp, &cm, c.Client.Scheme()) err = c.Client.Patch(ctx, &cm, client.Apply, &client.PatchOptions{FieldManager: "k0s-bootstrap"}) if err != nil { return fmt.Errorf("error creating ConfigMap: %w", err) @@ -642,7 +646,7 @@ token = ` + frpToken + ` Kind: "Deployment", }, ObjectMeta: metav1.ObjectMeta{ - Name: kcp.GetName() + "-frps", + Name: fmt.Sprintf(FRPDeploymentNameTemplate, kcp.GetName()), Namespace: kcp.GetNamespace(), }, Spec: appsv1.DeploymentSpec{ @@ -699,7 +703,7 @@ token = ` + frpToken + ` }}, }, } - _ = ctrl.SetControllerReference(kcp, &frpsDeployment, c.Scheme) + _ = ctrl.SetControllerReference(kcp, &frpsDeployment, c.Client.Scheme()) err = c.Client.Patch(ctx, &frpsDeployment, client.Apply, &client.PatchOptions{FieldManager: "k0s-bootstrap"}) if err != nil { return fmt.Errorf("error creating Deployment: %w", err) @@ -711,7 +715,7 @@ token = ` + frpToken + ` Kind: "Service", }, ObjectMeta: metav1.ObjectMeta{ - Name: kcp.GetName() + "-frps", + Name: fmt.Sprintf(FRPServiceNameTemplate, kcp.GetName()), Namespace: kcp.GetNamespace(), }, Spec: corev1.ServiceSpec{ @@ -735,7 +739,7 @@ token = ` + frpToken + ` Type: corev1.ServiceTypeNodePort, }, } - _ = ctrl.SetControllerReference(kcp, &frpsService, c.Scheme) + _ = ctrl.SetControllerReference(kcp, &frpsService, c.Client.Scheme()) err = c.Client.Patch(ctx, &frpsService, client.Apply, &client.PatchOptions{FieldManager: "k0s-bootstrap"}) if err != nil { return fmt.Errorf("error creating Service: %w", err) @@ -754,7 +758,7 @@ func (c *K0sController) detectNodeIP(ctx context.Context, _ *cpv1beta1.K0sContro } func (c *K0sController) createFRPToken(ctx context.Context, cluster *clusterv1.Cluster, kcp *cpv1beta1.K0sControlPlane) (string, error) { - secretName := cluster.Name + "-frp-token" + secretName := fmt.Sprintf(FRPTokenNameTemplate, cluster.Name) var existingSecret corev1.Secret err := c.Client.Get(ctx, client.ObjectKey{Name: secretName, Namespace: cluster.Namespace}, &existingSecret) @@ -783,7 +787,7 @@ func (c *K0sController) createFRPToken(ctx context.Context, cluster *clusterv1.C Type: clusterv1.ClusterSecretType, } - _ = ctrl.SetControllerReference(kcp, frpSecret, c.Scheme) + _ = ctrl.SetControllerReference(kcp, frpSecret, c.Client.Scheme()) return frpToken, c.Client.Patch(ctx, frpSecret, client.Apply, &client.PatchOptions{ FieldManager: "k0smotron", diff --git a/internal/controller/controlplane/k0s_controlplane_controller_test.go b/internal/controller/controlplane/k0s_controlplane_controller_test.go index 746f65617..22964c4f6 100644 --- a/internal/controller/controlplane/k0s_controlplane_controller_test.go +++ b/internal/controller/controlplane/k0s_controlplane_controller_test.go @@ -1,14 +1,45 @@ package controlplane import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" "testing" + "time" + . "github.com/onsi/gomega" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + restfake "k8s.io/client-go/rest/fake" + "k8s.io/client-go/tools/clientcmd/api" + clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/utils/ptr" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + kubeadmConfig "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" + "sigs.k8s.io/cluster-api/controllers/external" + "sigs.k8s.io/cluster-api/util" + "sigs.k8s.io/cluster-api/util/collections" + "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/cluster-api/util/kubeconfig" + "sigs.k8s.io/cluster-api/util/secret" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + autopilot "github.com/k0sproject/k0s/pkg/apis/autopilot/v1beta2" bootstrapv1 "github.com/k0sproject/k0smotron/api/bootstrap/v1beta1" "github.com/k0sproject/k0smotron/api/controlplane/v1beta1" + cpv1beta1 "github.com/k0sproject/k0smotron/api/controlplane/v1beta1" ) func TestK0sConfigEnrichment(t *testing.T) { @@ -103,3 +134,1202 @@ func TestK0sConfigEnrichment(t *testing.T) { }) } } + +func TestReconcileReturnErrorWhenOwnerClusterIsMissing(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-return-error-cluster-owner-missing") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, gmt := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + g.Expect(testEnv.Create(ctx, gmt)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, gmt, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).To(BeComparableTo(ctrl.Result{RequeueAfter: 20 * time.Second, Requeue: true})) + + g.Expect(testEnv.CleanupAndWait(ctx, cluster)).To(Succeed()) + + g.Eventually(func() error { + _, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)}) + return err + }, 10*time.Second).Should(HaveOccurred()) +} + +func TestReconcileNoK0sControlPlane(t *testing.T) { + g := NewWithT(t) + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-no-control-plane") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).To(BeComparableTo(ctrl.Result{})) +} + +func TestReconcilePausedCluster(t *testing.T) { + g := NewWithT(t) + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-paused-cluster") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + + // Cluster 'paused'. + cluster.Spec.Paused = true + + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).To(BeComparableTo(ctrl.Result{})) +} + +func TestReconcilePausedK0sControlPlane(t *testing.T) { + g := NewWithT(t) + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-paused-k0scontrolplane") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + // K0sControlPlane with 'paused' annotation. + kcp.Annotations = map[string]string{"cluster.x-k8s.io/paused": "true"} + + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + + result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).To(BeComparableTo(ctrl.Result{})) +} + +func TestReconcileTunneling(t *testing.T) { + g := NewWithT(t) + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-tunneling") + g.Expect(err).ToNot(HaveOccurred()) + + node := createNode() + g.Expect(testEnv.Create(ctx, node)).To(Succeed()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + kcp.Spec.K0sConfigSpec = bootstrapv1.K0sConfigSpec{ + Tunneling: bootstrapv1.TunnelingSpec{ + Enabled: true, + }, + } + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + clientSet, err := kubernetes.NewForConfig(testEnv.Config) + g.Expect(err).ToNot(HaveOccurred()) + + r := &K0sController{ + Client: testEnv, + ClientSet: clientSet, + } + err = r.reconcileTunneling(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + + frpToken, err := clientSet.CoreV1().Secrets(ns.Name).Get(ctx, fmt.Sprintf(FRPTokenNameTemplate, cluster.Name), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(metav1.IsControlledBy(frpToken, kcp)).To(BeTrue()) + + frpCM, err := clientSet.CoreV1().ConfigMaps(ns.Name).Get(ctx, fmt.Sprintf(FRPConfigMapNameTemplate, kcp.GetName()), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(metav1.IsControlledBy(frpCM, kcp)).To(BeTrue()) + + frpDeploy, err := clientSet.AppsV1().Deployments(ns.Name).Get(ctx, fmt.Sprintf(FRPDeploymentNameTemplate, kcp.GetName()), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(metav1.IsControlledBy(frpDeploy, kcp)).To(BeTrue()) + + frpService, err := clientSet.CoreV1().Services(ns.Name).Get(ctx, fmt.Sprintf(FRPServiceNameTemplate, kcp.GetName()), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(metav1.IsControlledBy(frpService, kcp)).To(BeTrue()) +} + +func TestReconcileKubeconfigEmptyAPIEndpoints(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-kubeconfig-empty-api-endpoints") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + + // Host and Port with zero values. + cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{} + + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + err = r.reconcileKubeconfig(ctx, cluster, kcp) + g.Expect(err).To(HaveOccurred()) + + kubeconfigSecret := &corev1.Secret{} + secretKey := client.ObjectKey{ + Namespace: cluster.Namespace, + Name: secret.Name(cluster.Name, secret.Kubeconfig), + } + g.Expect(testEnv.GetAPIReader().Get(ctx, secretKey, kubeconfigSecret)).To(MatchError(ContainSubstring("not found"))) +} + +func TestReconcileKubeconfigMissingCACertificate(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-kubeconfig-missing-ca-certificates") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + + err = r.reconcileKubeconfig(ctx, cluster, kcp) + g.Expect(err).To(HaveOccurred()) + + kubeconfigSecret := &corev1.Secret{} + secretKey := client.ObjectKey{ + Namespace: cluster.Namespace, + Name: secret.Name(cluster.Name, secret.Kubeconfig), + } + g.Expect(testEnv.GetAPIReader().Get(ctx, secretKey, kubeconfigSecret)).To(MatchError(ContainSubstring("not found"))) +} + +func TestReconcileKubeconfigTunnelingModeNotEnabled(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-kubeconfig-tunneling-mode-not-enabled") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + // Tunneling not enabled. + kcp.Spec.K0sConfigSpec.Tunneling.Enabled = false + + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + kubeconfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name(cluster.Name, secret.Kubeconfig), + Namespace: cluster.Namespace, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + }, + OwnerReferences: []metav1.OwnerReference{}, + }, + Data: map[string][]byte{ + secret.KubeconfigDataName: {}, + }, + } + g.Expect(testEnv.Create(ctx, kubeconfigSecret)).To(Succeed()) + + r := &K0sController{ + Client: testEnv, + } + + err = r.reconcileKubeconfig(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) +} + +func TestReconcileKubeconfigTunnelingModeProxy(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-kubeconfig-tunneling-mode-proxy") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + // Tunneling mode = 'proxy' + kcp.Spec.K0sConfigSpec.Tunneling = bootstrapv1.TunnelingSpec{ + Enabled: true, + Mode: "proxy", + ServerAddress: "test.com", + TunnelingNodePort: 9999, + } + + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + kubeconfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name(cluster.Name, secret.Kubeconfig), + Namespace: cluster.Namespace, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + }, + OwnerReferences: []metav1.OwnerReference{}, + }, + Data: map[string][]byte{ + secret.KubeconfigDataName: {}, + }, + } + g.Expect(testEnv.Create(ctx, kubeconfigSecret)).To(Succeed()) + + clusterCerts := secret.NewCertificatesForInitialControlPlane(&kubeadmConfig.ClusterConfiguration{}) + g.Expect(clusterCerts.Generate()).To(Succeed()) + caCert := clusterCerts.GetByPurpose(secret.ClusterCA) + caCertSecret := caCert.AsSecret( + client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, + *metav1.NewControllerRef(kcp, cpv1beta1.GroupVersion.WithKind("K0sControlPlane")), + ) + g.Expect(testEnv.Create(ctx, caCertSecret)).To(Succeed()) + + r := &K0sController{ + Client: testEnv, + } + + err = r.reconcileKubeconfig(ctx, cluster, kcp) + g.Expect(err).To(HaveOccurred()) + + secretKey := client.ObjectKey{ + Namespace: cluster.Namespace, + Name: secret.Name(cluster.Name+"-proxied", secret.Kubeconfig), + } + + kubeconfigProxiedSecret := &corev1.Secret{} + g.Expect(testEnv.Get(ctx, secretKey, kubeconfigProxiedSecret)).To(Succeed()) + + kubeconfigProxiedSecretCrt, _ := runtime.Decode(clientcmdlatest.Codec, kubeconfigProxiedSecret.Data["value"]) + for _, v := range kubeconfigProxiedSecretCrt.(*api.Config).Clusters { + g.Expect(v.Server).To(Equal("https://test.endpoint:6443")) + g.Expect(v.ProxyURL).To(Equal("http://test.com:9999")) + } +} + +func TestReconcileKubeconfigTunnelingModeTunnel(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-kubeconfig-tunneling-mode-tunnel") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + // Tunneling mode = 'tunnel' + kcp.Spec.K0sConfigSpec.Tunneling = bootstrapv1.TunnelingSpec{ + Enabled: true, + Mode: "tunnel", + ServerAddress: "test.com", + TunnelingNodePort: 9999, + } + + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + kubeconfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name(cluster.Name, secret.Kubeconfig), + Namespace: cluster.Namespace, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + }, + OwnerReferences: []metav1.OwnerReference{}, + }, + Data: map[string][]byte{ + secret.KubeconfigDataName: {}, + }, + } + g.Expect(testEnv.Create(ctx, kubeconfigSecret)).To(Succeed()) + + clusterCerts := secret.NewCertificatesForInitialControlPlane(&kubeadmConfig.ClusterConfiguration{}) + g.Expect(clusterCerts.Generate()).To(Succeed()) + caCert := clusterCerts.GetByPurpose(secret.ClusterCA) + caCertSecret := caCert.AsSecret( + client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, + *metav1.NewControllerRef(kcp, cpv1beta1.GroupVersion.WithKind("K0sControlPlane")), + ) + g.Expect(testEnv.Create(ctx, caCertSecret)).To(Succeed()) + + r := &K0sController{ + Client: testEnv, + } + + err = r.reconcileKubeconfig(ctx, cluster, kcp) + g.Expect(err).To(HaveOccurred()) + + secretKey := client.ObjectKey{ + Namespace: cluster.Namespace, + Name: secret.Name(cluster.Name+"-tunneled", secret.Kubeconfig), + } + kubeconfigProxiedSecret := &corev1.Secret{} + g.Expect(testEnv.Get(ctx, secretKey, kubeconfigProxiedSecret)).ToNot(HaveOccurred()) + + kubeconfigProxiedSecretCrt, _ := runtime.Decode(clientcmdlatest.Codec, kubeconfigProxiedSecret.Data["value"]) + for _, v := range kubeconfigProxiedSecretCrt.(*api.Config).Clusters { + g.Expect(v.Server).To(Equal("https://test.com:9999")) + } +} + +func TestReconcileK0sConfigNotProvided(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-config-k0sconfig-not-provided") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + kcp.Spec.K0sConfigSpec.K0s = nil + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + err = r.reconcileConfig(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(kcp.Spec.K0sConfigSpec.K0s).To(BeNil()) +} + +func TestReconcileK0sConfigWithNLLBEnabled(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-config-nllb-enabled") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + // Enable '.spec.network.nodeLocalLoadBalancing' + kcp.Spec.K0sConfigSpec = bootstrapv1.K0sConfigSpec{ + K0s: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "k0s.k0sproject.io/v1beta1", + "kind": "ClusterConfig", + "spec": map[string]interface{}{ + "api": map[string]interface{}{ + "sans": []interface{}{ + "test.com", + }, + }, + "network": map[string]interface{}{ + "nodeLocalLoadBalancing": map[string]interface{}{ + "enabled": true, + }, + }, + }, + }, + }, + } + + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + err = r.reconcileConfig(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + + expectedk0sConfig := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "k0s.k0sproject.io/v1beta1", + "kind": "ClusterConfig", + "spec": map[string]interface{}{ + "api": map[string]interface{}{ + "sans": []interface{}{ + "test.endpoint", + "test.com", + }, + }, + "network": map[string]interface{}{ + "nodeLocalLoadBalancing": map[string]interface{}{ + "enabled": true, + }, + }, + }, + }, + } + g.Expect(kcp.Spec.K0sConfigSpec.K0s).To(Equal(expectedk0sConfig)) +} + +func TestReconcileK0sConfigWithNLLBDisabled(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-config-nllb-disabled") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + // Disable '.spec.network.nodeLocalLoadBalancing' + kcp.Spec.K0sConfigSpec = bootstrapv1.K0sConfigSpec{ + K0s: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "k0s.k0sproject.io/v1beta1", + "kind": "ClusterConfig", + "spec": map[string]interface{}{ + "api": map[string]interface{}{ + "sans": []interface{}{ + "test.com", + }, + }, + }, + }, + }, + } + + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + err = r.reconcileConfig(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + + expectedk0sConfig := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "k0s.k0sproject.io/v1beta1", + "kind": "ClusterConfig", + "spec": map[string]interface{}{ + "api": map[string]interface{}{ + "sans": []interface{}{ + "test.com", + }, + "externalAddress": "test.endpoint", + }, + }, + }, + } + g.Expect(kcp.Spec.K0sConfigSpec.K0s).To(Equal(expectedk0sConfig)) +} + +func TestReconcileK0sConfigTunnelingServerAddressToApiSans(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-config-tunneling-serveraddress-to-api-sans") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, _ := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + + // With '.spec.k0sConfigSpec.Tunneling.ServerAddress' + kcp.Spec.K0sConfigSpec = bootstrapv1.K0sConfigSpec{ + K0s: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "k0s.k0sproject.io/v1beta1", + "kind": "ClusterConfig", + "spec": map[string]interface{}{ + "api": map[string]interface{}{ + "sans": []interface{}{ + "test.com", + }, + }, + }, + }, + }, + Tunneling: bootstrapv1.TunnelingSpec{ + ServerAddress: "my-tunneling-server-address.com", + }, + } + + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, cluster, ns) + + r := &K0sController{ + Client: testEnv, + } + err = r.reconcileConfig(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + + expectedk0sConfig := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "k0s.k0sproject.io/v1beta1", + "kind": "ClusterConfig", + "spec": map[string]interface{}{ + "api": map[string]interface{}{ + "sans": []interface{}{ + "test.com", + "my-tunneling-server-address.com", + }, + "externalAddress": "test.endpoint", + }, + }, + }, + } + g.Expect(kcp.Spec.K0sConfigSpec.K0s).To(Equal(expectedk0sConfig)) +} + +func TestReconcileMachinesScaleUp(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-machine-scale-up") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, gmt := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + g.Expect(testEnv.Create(ctx, gmt)).To(Succeed()) + + desiredReplicas := 5 + kcp.Spec.Replicas = int32(desiredReplicas) + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, gmt, cluster, ns) + + kcpOwnerRef := *metav1.NewControllerRef(kcp, cpv1beta1.GroupVersion.WithKind("K0sControlPlane")) + + r := &K0sController{ + Client: testEnv, + } + + firstMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 0), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + firstMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, firstMachineRelatedToControlPlane)).To(Succeed()) + + secondMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 1), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + secondMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, secondMachineRelatedToControlPlane)).To(Succeed()) + + machineNotRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "external-machine", + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + g.Expect(testEnv.Create(ctx, machineNotRelatedToControlPlane)).To(Succeed()) + + _, err = r.reconcileMachines(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + + machines, err := collections.GetFilteredMachinesForCluster(ctx, testEnv, cluster, collections.ControlPlaneMachines(cluster.Name), collections.ActiveMachines) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(machines).To(HaveLen(desiredReplicas)) + for _, m := range machines { + expectedLabels := map[string]string{ + clusterv1.ClusterNameLabel: cluster.GetName(), + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + } + g.Expect(m.Labels).Should(Equal(expectedLabels)) + g.Expect(metav1.IsControlledBy(m, kcp)).To(BeTrue()) + g.Expect(*m.Spec.Version).Should(Equal(kcp.Spec.Version)) + } +} + +func TestReconcileMachinesScaleDown(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-machines-scale-down") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, gmt := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + g.Expect(testEnv.Create(ctx, gmt)).To(Succeed()) + + desiredReplicas := 1 + kcp.Spec.Replicas = int32(desiredReplicas) + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, gmt, cluster, ns) + + kcpOwnerRef := *metav1.NewControllerRef(kcp, cpv1beta1.GroupVersion.WithKind("K0sControlPlane")) + + fakeClient := &restfake.RESTClient{ + Client: restfake.CreateHTTPClient(roundTripperForWorkloadClusterAPI), + } + + restClient, _ := rest.RESTClientFor(&rest.Config{ + ContentConfig: rest.ContentConfig{ + NegotiatedSerializer: scheme.Codecs, + GroupVersion: &metav1.SchemeGroupVersion, + }, + }) + restClient.Client = fakeClient.Client + + r := &K0sController{ + Client: testEnv, + workloadClusterKubeClient: kubernetes.New(restClient), + } + + firstMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 0), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + firstMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, firstMachineRelatedToControlPlane)).To(Succeed()) + + secondMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 1), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + secondMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, secondMachineRelatedToControlPlane)).To(Succeed()) + + thirdMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 2), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + thirdMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, thirdMachineRelatedToControlPlane)).To(Succeed()) + + machineNotRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "external-machine", + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + g.Expect(testEnv.Create(ctx, machineNotRelatedToControlPlane)).To(Succeed()) + + g.Eventually(func(g Gomega) { + _, err = r.reconcileMachines(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + + machines, err := collections.GetFilteredMachinesForCluster(ctx, testEnv, cluster, collections.ControlPlaneMachines(cluster.Name), collections.ActiveMachines) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(machines).To(HaveLen(desiredReplicas)) + + k0sBootstrapConfigList := &bootstrapv1.K0sControllerConfigList{} + g.Expect(testEnv.GetAPIReader().List(ctx, k0sBootstrapConfigList, client.InNamespace(cluster.Namespace))).To(Succeed()) + g.Expect(k0sBootstrapConfigList.Items).To(HaveLen(desiredReplicas)) + + for _, m := range machines { + expectedLabels := map[string]string{ + clusterv1.ClusterNameLabel: cluster.GetName(), + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + } + g.Expect(m.Labels).Should(Equal(expectedLabels)) + g.Expect(metav1.IsControlledBy(m, kcp)).To(BeTrue()) + g.Expect(*m.Spec.Version).Should(Equal(kcp.Spec.Version)) + + // verify that the bootrap config related to the existing machines is present. + bootstrapObjectKey := client.ObjectKey{ + Namespace: m.Namespace, + Name: m.Name, + } + kc := &bootstrapv1.K0sControllerConfig{} + g.Expect(testEnv.GetAPIReader().Get(ctx, bootstrapObjectKey, kc)).ToNot(HaveOccurred()) + g.Expect(metav1.IsControlledBy(kc, m)).To(BeTrue()) + } + }).Should(Succeed()) +} + +func TestReconcileMachinesSyncOldMachines(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-machines-sync-old-machines") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, gmt := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + g.Expect(testEnv.Create(ctx, gmt)).To(Succeed()) + + desiredReplicas := 3 + kcp.Spec.Replicas = int32(desiredReplicas) + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, gmt, cluster, ns) + + kcpOwnerRef := *metav1.NewControllerRef(kcp, cpv1beta1.GroupVersion.WithKind("K0sControlPlane")) + + fakeClient := &restfake.RESTClient{ + Client: restfake.CreateHTTPClient(roundTripperForWorkloadClusterAPI), + } + + restClient, _ := rest.RESTClientFor(&rest.Config{ + ContentConfig: rest.ContentConfig{ + NegotiatedSerializer: scheme.Codecs, + GroupVersion: &metav1.SchemeGroupVersion, + }, + }) + restClient.Client = fakeClient.Client + + clientSet, err := kubernetes.NewForConfig(testEnv.Config) + g.Expect(err).ToNot(HaveOccurred()) + + r := &K0sController{ + Client: testEnv, + workloadClusterKubeClient: kubernetes.New(restClient), + ClientSet: clientSet, + } + + firstMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 0), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.29.0"), + }, + } + firstMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, firstMachineRelatedToControlPlane)).To(Succeed()) + + secondMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 1), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.30.0"), + }, + } + secondMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, secondMachineRelatedToControlPlane)).To(Succeed()) + + thirdMachineRelatedToControlPlane := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: machineName(kcp.Name, 2), + Namespace: ns.Name, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: cluster.Name, + Version: ptr.To("v1.29.0"), + }, + } + thirdMachineRelatedToControlPlane.SetOwnerReferences([]metav1.OwnerReference{kcpOwnerRef}) + g.Expect(testEnv.Create(ctx, thirdMachineRelatedToControlPlane)).To(Succeed()) + + g.Eventually(func(g Gomega) { + _, err = r.reconcileMachines(ctx, cluster, kcp) + g.Expect(err).ToNot(HaveOccurred()) + + machines, err := collections.GetFilteredMachinesForCluster(ctx, testEnv, cluster, collections.ControlPlaneMachines(cluster.Name), collections.ActiveMachines) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(machines).To(HaveLen(desiredReplicas)) + + k0sBootstrapConfigList := &bootstrapv1.K0sControllerConfigList{} + g.Expect(testEnv.GetAPIReader().List(ctx, k0sBootstrapConfigList, client.InNamespace(cluster.Namespace))).To(Succeed()) + g.Expect(k0sBootstrapConfigList.Items).To(HaveLen(desiredReplicas)) + + for _, m := range machines { + expectedLabels := map[string]string{ + clusterv1.ClusterNameLabel: cluster.GetName(), + clusterv1.MachineControlPlaneLabel: "true", + generatedMachineRoleLabel: "control-plane", + } + g.Expect(m.Labels).Should(Equal(expectedLabels)) + g.Expect(metav1.IsControlledBy(m, kcp)).To(BeTrue()) + g.Expect(*m.Spec.Version).Should(Equal(kcp.Spec.Version)) + + // verify that the bootrap config related to the existing machines is present. + bootstrapObjectKey := client.ObjectKey{ + Namespace: m.Namespace, + Name: m.Name, + } + kc := &bootstrapv1.K0sControllerConfig{} + g.Expect(testEnv.GetAPIReader().Get(ctx, bootstrapObjectKey, kc)).ToNot(HaveOccurred()) + g.Expect(metav1.IsControlledBy(kc, m)).To(BeTrue()) + } + }, 5*time.Second).Should(Succeed()) +} + +func TestReconcileInitializeControlPlanes(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "test-reconcile-initialize-controlplanes") + g.Expect(err).ToNot(HaveOccurred()) + + cluster, kcp, gmt := createClusterWithControlPlane(ns.Name) + g.Expect(testEnv.Create(ctx, cluster)).To(Succeed()) + kcp.Spec.Replicas = 1 + g.Expect(testEnv.Create(ctx, kcp)).To(Succeed()) + g.Expect(testEnv.Create(ctx, gmt)).To(Succeed()) + + defer func(do ...client.Object) { + g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed()) + }(kcp, gmt, cluster, ns) + + expectedLabels := map[string]string{clusterv1.ClusterNameLabel: cluster.Name} + + fakeClient := &restfake.RESTClient{ + Client: restfake.CreateHTTPClient(roundTripperForWorkloadClusterAPI), + } + + restClient, _ := rest.RESTClientFor(&rest.Config{ + ContentConfig: rest.ContentConfig{ + NegotiatedSerializer: scheme.Codecs, + GroupVersion: &metav1.SchemeGroupVersion, + }, + }) + restClient.Client = fakeClient.Client + + r := &K0sController{ + Client: testEnv, + workloadClusterKubeClient: kubernetes.New(restClient), + } + + _, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)}) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(testEnv.GetAPIReader().Get(ctx, client.ObjectKey{Name: kcp.Name, Namespace: kcp.Namespace}, kcp)).To(Succeed()) + g.Expect(kcp.Status.Selector).NotTo(BeEmpty()) + g.Expect(kcp.Status.Version).To(Equal(fmt.Sprintf("%s+%s", kcp.Spec.Version, defaultK0sSuffix))) + g.Expect(kcp.Status.Replicas).To(BeEquivalentTo(1)) + g.Expect(testEnv.GetAPIReader().Get(ctx, util.ObjectKey(gmt), gmt)).To(Succeed()) + g.Expect(gmt.GetOwnerReferences()).To(ContainElement(metav1.OwnerReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: cluster.Name, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + UID: cluster.UID, + })) + g.Expect(conditions.IsFalse(kcp, cpv1beta1.ControlPlaneReadyCondition)).To(BeTrue()) + + // Expected secrets are created + caSecret, err := secret.GetFromNamespacedName(ctx, testEnv, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, secret.ClusterCA) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(caSecret).NotTo(BeNil()) + g.Expect(caSecret.Data).NotTo(BeEmpty()) + g.Expect(caSecret.Labels).To(Equal(expectedLabels)) + + etcdSecret, err := secret.GetFromNamespacedName(ctx, testEnv, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, secret.EtcdCA) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(etcdSecret).NotTo(BeNil()) + g.Expect(etcdSecret.Data).NotTo(BeEmpty()) + g.Expect(etcdSecret.Labels).To(Equal(expectedLabels)) + + kubeconfigSecret, err := secret.GetFromNamespacedName(ctx, testEnv, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, secret.Kubeconfig) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(kubeconfigSecret).NotTo(BeNil()) + g.Expect(kubeconfigSecret.Data).NotTo(BeEmpty()) + g.Expect(kubeconfigSecret.Labels).To(Equal(expectedLabels)) + k, err := kubeconfig.FromSecret(ctx, testEnv, util.ObjectKey(cluster)) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(k).NotTo(BeEmpty()) + + proxySecret, err := secret.GetFromNamespacedName(ctx, testEnv, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, secret.FrontProxyCA) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(proxySecret).NotTo(BeNil()) + g.Expect(proxySecret.Data).NotTo(BeEmpty()) + g.Expect(proxySecret.Labels).To(Equal(expectedLabels)) + + saSecret, err := secret.GetFromNamespacedName(ctx, testEnv, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, secret.ServiceAccount) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(saSecret).NotTo(BeNil()) + g.Expect(saSecret.Data).NotTo(BeEmpty()) + g.Expect(saSecret.Labels).To(Equal(expectedLabels)) + + machineList := &clusterv1.MachineList{} + g.Expect(testEnv.GetAPIReader().List(ctx, machineList, client.InNamespace(cluster.Namespace))).To(Succeed()) + g.Expect(machineList.Items).To(HaveLen(1)) + machine := machineList.Items[0] + g.Expect(machine.Name).To(HavePrefix(kcp.Name)) + g.Expect(*machine.Spec.Version).To(Equal(fmt.Sprintf("%s+%s", kcp.Spec.Version, defaultK0sSuffix))) + // Newly cloned infra objects should have the infraref annotation. + infraObj, err := external.Get(ctx, r.Client, &machine.Spec.InfrastructureRef, machine.Spec.InfrastructureRef.Namespace) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(infraObj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromNameAnnotation, gmt.GetName())) + g.Expect(infraObj.GetAnnotations()).To(HaveKeyWithValue(clusterv1.TemplateClonedFromGroupKindAnnotation, gmt.GroupVersionKind().GroupKind().String())) + + k0sBootstrapConfigList := &bootstrapv1.K0sControllerConfigList{} + g.Expect(testEnv.GetAPIReader().List(ctx, k0sBootstrapConfigList, client.InNamespace(cluster.Namespace))).To(Succeed()) + g.Expect(k0sBootstrapConfigList.Items).To(HaveLen(1)) + + g.Expect(metav1.IsControlledBy(&k0sBootstrapConfigList.Items[0], &machine)).To(BeTrue()) + +} + +func roundTripperForWorkloadClusterAPI(req *http.Request) (*http.Response, error) { + header := http.Header{} + header.Set("Content-Type", runtime.ContentTypeJSON) + + switch req.Method { + case "GET": + if strings.HasPrefix(req.URL.Path, "/apis/autopilot.k0sproject.io/v1beta2/controlnodes/") { + res, err := json.Marshal(autopilot.ControlNode{}) + if err != nil { + return nil, err + } + return &http.Response{StatusCode: http.StatusOK, Header: header, Body: io.NopCloser(bytes.NewReader(res))}, nil + + } + case "DELETE": + if strings.HasPrefix(req.URL.Path, "/apis/autopilot.k0sproject.io/v1beta2/controlnodes/") { + return &http.Response{StatusCode: http.StatusOK, Header: header, Body: nil}, nil + } + case "PATCH": + switch { + case strings.HasPrefix(req.URL.Path, "/apis/etcd.k0sproject.io/v1beta1/etcdmembers/"): + { + return &http.Response{StatusCode: http.StatusOK, Header: header, Body: nil}, nil + } + case strings.HasPrefix(req.URL.Path, "/apis/autopilot.k0sproject.io/v1beta2/controlnodes/"): + { + return &http.Response{StatusCode: http.StatusOK, Header: header, Body: nil}, nil + } + case strings.HasPrefix(req.URL.Path, "/apis/infrastructure.cluster.x-k8s.io/v1beta1/namespaces/"): + { + return &http.Response{StatusCode: http.StatusOK, Header: header, Body: nil}, nil + } + } + } + + return &http.Response{StatusCode: http.StatusNotFound, Header: header, Body: nil}, nil +} + +func newCluster(namespacedName *types.NamespacedName) *clusterv1.Cluster { + return &clusterv1.Cluster{ + TypeMeta: metav1.TypeMeta{ + Kind: "Cluster", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespacedName.Namespace, + Name: namespacedName.Name, + }, + } +} + +func createNode() *v1.Node { + return &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"node-role.kubernetes.io/control-plane": ""}, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "1.1.1.1", + }, + }, + }, + } +} + +func createClusterWithControlPlane(namespace string) (*clusterv1.Cluster, *cpv1beta1.K0sControlPlane, *unstructured.Unstructured) { + kcpName := fmt.Sprintf("kcp-foo-%s", util.RandomString(6)) + + cluster := newCluster(&types.NamespacedName{Name: kcpName, Namespace: namespace}) + cluster.Spec = clusterv1.ClusterSpec{ + ControlPlaneRef: &v1.ObjectReference{ + Kind: "K0sControlPlane", + Namespace: namespace, + Name: kcpName, + APIVersion: cpv1beta1.GroupVersion.String(), + }, + ControlPlaneEndpoint: clusterv1.APIEndpoint{ + Host: "test.endpoint", + Port: 6443, + }, + } + + kcp := &cpv1beta1.K0sControlPlane{ + TypeMeta: metav1.TypeMeta{ + APIVersion: cpv1beta1.GroupVersion.String(), + Kind: "K0sControlPlane", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: kcpName, + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "Cluster", + APIVersion: clusterv1.GroupVersion.String(), + Name: kcpName, + UID: "1", + }, + }, + }, + Spec: v1beta1.K0sControlPlaneSpec{ + MachineTemplate: &v1beta1.K0sControlPlaneMachineTemplate{ + InfrastructureRef: v1.ObjectReference{ + Kind: "GenericInfrastructureMachineTemplate", + Namespace: namespace, + Name: "infra-foo", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", + }, + }, + UpdateStrategy: cpv1beta1.UpdateRecreate, + Replicas: int32(1), + Version: "v1.30.0", + }, + } + + genericMachineTemplate := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": "GenericInfrastructureMachineTemplate", + "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", + "metadata": map[string]interface{}{ + "name": "infra-foo", + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "hello": "world", + }, + }, + }, + }, + } + return cluster, kcp, genericMachineTemplate +} diff --git a/internal/controller/controlplane/suite_test.go b/internal/controller/controlplane/suite_test.go new file mode 100644 index 000000000..cf2af4a22 --- /dev/null +++ b/internal/controller/controlplane/suite_test.go @@ -0,0 +1,37 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controlplane + +import ( + "os" + "testing" + + "github.com/k0sproject/k0smotron/internal/test/envtest" + ctrl "sigs.k8s.io/controller-runtime" +) + +var ( + testEnv *envtest.Environment + ctx = ctrl.SetupSignalHandler() +) + +func TestMain(m *testing.M) { + testEnv = envtest.Build(ctx) + code := m.Run() + testEnv.Teardown() + os.Exit(code) +} diff --git a/internal/controller/controlplane/util.go b/internal/controller/controlplane/util.go index 92fe0a083..dcb7fe8f9 100644 --- a/internal/controller/controlplane/util.go +++ b/internal/controller/controlplane/util.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/imdario/mergo" + + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -17,6 +19,7 @@ import ( "sigs.k8s.io/cluster-api/util/kubeconfig" "sigs.k8s.io/cluster-api/util/secret" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" cpv1beta1 "github.com/k0sproject/k0smotron/api/controlplane/v1beta1" k0smoutil "github.com/k0sproject/k0smotron/internal/controller/util" @@ -89,10 +92,24 @@ func (c *K0sController) createKubeconfigSecret(ctx context.Context, cfg *api.Con kcSecret := kubeconfig.GenerateSecretWithOwner(clusterName, cfgBytes, owner) kcSecret.Name = secretName - return c.Create(ctx, kcSecret) + errCreate := c.Create(ctx, kcSecret) + + log := log.FromContext(ctx).WithValues("TEST", "check if secret is created") + + all := &corev1.SecretList{} + _ = c.List(ctx, all, &client.ListOptions{Namespace: cluster.Namespace}) + for _, item := range all.Items { + log.Info(fmt.Sprintf("createdSecret=%s, secretName=%s, secretNamespace=%s, %v", kcSecret.Name, item.Name, item.Namespace, errCreate)) + } + + return errCreate } func (c *K0sController) getKubeClient(ctx context.Context, cluster *clusterv1.Cluster) (*kubernetes.Clientset, error) { + if c.workloadClusterKubeClient != nil { + return c.workloadClusterKubeClient, nil + } + return k0smoutil.GetKubeClient(ctx, c.Client, cluster) } diff --git a/internal/controller/k0smotron.io/k0smotroncluster_etcd.go b/internal/controller/k0smotron.io/k0smotroncluster_etcd.go index e8e587a42..4f092cd99 100644 --- a/internal/controller/k0smotron.io/k0smotroncluster_etcd.go +++ b/internal/controller/k0smotron.io/k0smotroncluster_etcd.go @@ -20,10 +20,11 @@ import ( "bytes" "context" "fmt" - batchv1 "k8s.io/api/batch/v1" "strings" "text/template" + batchv1 "k8s.io/api/batch/v1" + km "github.com/k0sproject/k0smotron/api/k0smotron.io/v1beta1" apps "k8s.io/api/apps/v1" diff --git a/internal/test/builder/bootstrap.go b/internal/test/builder/bootstrap.go new file mode 100644 index 000000000..1daf6fa39 --- /dev/null +++ b/internal/test/builder/bootstrap.go @@ -0,0 +1,104 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // BootstrapGroupVersion is group version used for bootstrap objects. + BootstrapGroupVersion = schema.GroupVersion{Group: "bootstrap.cluster.x-k8s.io", Version: "v1beta1"} + + // GenericBootstrapConfigKind is the Kind for the GenericBootstrapConfig. + GenericBootstrapConfigKind = "GenericBootstrapConfig" + // GenericBootstrapConfigCRD is a generic bootstrap CRD. + GenericBootstrapConfigCRD = untypedCRD(BootstrapGroupVersion.WithKind(GenericBootstrapConfigKind)) + + // GenericBootstrapConfigTemplateKind is the Kind for the GenericBootstrapConfigTemplate. + GenericBootstrapConfigTemplateKind = "GenericBootstrapConfigTemplate" + // GenericBootstrapConfigTemplateCRD is a generic bootstrap template CRD. + GenericBootstrapConfigTemplateCRD = untypedCRD(BootstrapGroupVersion.WithKind(GenericBootstrapConfigTemplateKind)) + + // TODO: drop generic CRDs in favour of typed test CRDs. + + // TestBootstrapConfigTemplateKind is the kind for the TestBootstrapConfigTemplate type. + TestBootstrapConfigTemplateKind = "TestBootstrapConfigTemplate" + // TestBootstrapConfigTemplateCRD is a test bootstrap config template CRD. + TestBootstrapConfigTemplateCRD = testBootstrapConfigTemplateCRD(BootstrapGroupVersion.WithKind(TestBootstrapConfigTemplateKind)) + + // TestBootstrapConfigKind is the kind for the TestBootstrapConfig type. + TestBootstrapConfigKind = "TestBootstrapConfig" + // TestBootstrapConfigCRD is a test bootstrap config CRD. + TestBootstrapConfigCRD = testBootstrapConfigCRD(BootstrapGroupVersion.WithKind(TestBootstrapConfigKind)) +) + +func testBootstrapConfigTemplateCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "template": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "spec": bootstrapConfigSpecSchema, + }, + }, + }, + }, + }) +} + +func testBootstrapConfigCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": bootstrapConfigSpecSchema, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // mandatory field from the Cluster API contract + "ready": {Type: "boolean"}, + "dataSecretName": {Type: "string"}, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + }, + }) +} + +var ( + bootstrapConfigSpecSchema = apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + } +) diff --git a/internal/test/builder/builders.go b/internal/test/builder/builders.go new file mode 100644 index 000000000..24e00c201 --- /dev/null +++ b/internal/test/builder/builders.go @@ -0,0 +1,1989 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" +) + +// ClusterBuilder holds the variables and objects required to build a clusterv1.Cluster. +type ClusterBuilder struct { + namespace string + name string + labels map[string]string + annotations map[string]string + topology *clusterv1.Topology + infrastructureCluster *unstructured.Unstructured + controlPlane *unstructured.Unstructured + network *clusterv1.ClusterNetwork +} + +// Cluster returns a ClusterBuilder with the given name and namespace. +func Cluster(namespace, name string) *ClusterBuilder { + return &ClusterBuilder{ + namespace: namespace, + name: name, + } +} + +// WithClusterNetwork sets the ClusterNetwork for the ClusterBuilder. +func (c *ClusterBuilder) WithClusterNetwork(clusterNetwork *clusterv1.ClusterNetwork) *ClusterBuilder { + c.network = clusterNetwork + return c +} + +// WithLabels sets the labels for the ClusterBuilder. +func (c *ClusterBuilder) WithLabels(labels map[string]string) *ClusterBuilder { + c.labels = labels + return c +} + +// WithAnnotations sets the annotations for the ClusterBuilder. +func (c *ClusterBuilder) WithAnnotations(annotations map[string]string) *ClusterBuilder { + c.annotations = annotations + return c +} + +// WithInfrastructureCluster adds the passed InfrastructureCluster to the ClusterBuilder. +func (c *ClusterBuilder) WithInfrastructureCluster(t *unstructured.Unstructured) *ClusterBuilder { + c.infrastructureCluster = t + return c +} + +// WithControlPlane adds the passed ControlPlane to the ClusterBuilder. +func (c *ClusterBuilder) WithControlPlane(t *unstructured.Unstructured) *ClusterBuilder { + c.controlPlane = t + return c +} + +// WithTopology adds the passed Topology object to the ClusterBuilder. +func (c *ClusterBuilder) WithTopology(topology *clusterv1.Topology) *ClusterBuilder { + c.topology = topology + return c +} + +// Build returns a Cluster with the attributes added to the ClusterBuilder. +func (c *ClusterBuilder) Build() *clusterv1.Cluster { + obj := &clusterv1.Cluster{ + TypeMeta: metav1.TypeMeta{ + Kind: "Cluster", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: c.name, + Namespace: c.namespace, + Labels: c.labels, + Annotations: c.annotations, + }, + Spec: clusterv1.ClusterSpec{ + Topology: c.topology, + ClusterNetwork: c.network, + }, + } + if c.infrastructureCluster != nil { + obj.Spec.InfrastructureRef = objToRef(c.infrastructureCluster) + } + if c.controlPlane != nil { + obj.Spec.ControlPlaneRef = objToRef(c.controlPlane) + } + return obj +} + +// ClusterTopologyBuilder contains the fields needed to build a testable ClusterTopology. +type ClusterTopologyBuilder struct { + class string + workers *clusterv1.WorkersTopology + version string + controlPlaneReplicas int32 + controlPlaneMHC *clusterv1.MachineHealthCheckTopology + variables []clusterv1.ClusterVariable +} + +// ClusterTopology returns a ClusterTopologyBuilder. +func ClusterTopology() *ClusterTopologyBuilder { + return &ClusterTopologyBuilder{ + workers: &clusterv1.WorkersTopology{}, + } +} + +// WithClass adds the passed ClusterClass name to the ClusterTopologyBuilder. +func (c *ClusterTopologyBuilder) WithClass(class string) *ClusterTopologyBuilder { + c.class = class + return c +} + +// WithVersion adds the passed version to the ClusterTopologyBuilder. +func (c *ClusterTopologyBuilder) WithVersion(version string) *ClusterTopologyBuilder { + c.version = version + return c +} + +// WithControlPlaneReplicas adds the passed replicas value to the ClusterTopologyBuilder. +func (c *ClusterTopologyBuilder) WithControlPlaneReplicas(replicas int32) *ClusterTopologyBuilder { + c.controlPlaneReplicas = replicas + return c +} + +// WithControlPlaneMachineHealthCheck adds MachineHealthCheckTopology used as the MachineHealthCheck value. +func (c *ClusterTopologyBuilder) WithControlPlaneMachineHealthCheck(mhc *clusterv1.MachineHealthCheckTopology) *ClusterTopologyBuilder { + c.controlPlaneMHC = mhc + return c +} + +// WithMachineDeployment passes the full MachineDeploymentTopology and adds it to an existing list in the ClusterTopologyBuilder. +func (c *ClusterTopologyBuilder) WithMachineDeployment(mdc clusterv1.MachineDeploymentTopology) *ClusterTopologyBuilder { + c.workers.MachineDeployments = append(c.workers.MachineDeployments, mdc) + return c +} + +// WithMachinePool passes the full MachinePoolTopology and adds it to an existing list in the ClusterTopologyBuilder. +func (c *ClusterTopologyBuilder) WithMachinePool(mpc clusterv1.MachinePoolTopology) *ClusterTopologyBuilder { + c.workers.MachinePools = append(c.workers.MachinePools, mpc) + return c +} + +// WithVariables adds the passed variables to the ClusterTopologyBuilder. +func (c *ClusterTopologyBuilder) WithVariables(vars ...clusterv1.ClusterVariable) *ClusterTopologyBuilder { + c.variables = vars + return c +} + +// Build returns a testable cluster Topology object with any values passed to the builder. +func (c *ClusterTopologyBuilder) Build() *clusterv1.Topology { + return &clusterv1.Topology{ + Class: c.class, + Workers: c.workers, + Version: c.version, + ControlPlane: clusterv1.ControlPlaneTopology{ + Replicas: &c.controlPlaneReplicas, + MachineHealthCheck: c.controlPlaneMHC, + }, + Variables: c.variables, + } +} + +// MachineDeploymentTopologyBuilder holds the values needed to create a testable MachineDeploymentTopology. +type MachineDeploymentTopologyBuilder struct { + class string + name string + replicas *int32 + mhc *clusterv1.MachineHealthCheckTopology + variables []clusterv1.ClusterVariable +} + +// MachineDeploymentTopology returns a builder used to create a testable MachineDeploymentTopology. +func MachineDeploymentTopology(name string) *MachineDeploymentTopologyBuilder { + return &MachineDeploymentTopologyBuilder{ + name: name, + } +} + +// WithClass adds a class string used as the MachineDeploymentTopology class. +func (m *MachineDeploymentTopologyBuilder) WithClass(class string) *MachineDeploymentTopologyBuilder { + m.class = class + return m +} + +// WithReplicas adds a replicas value used as the MachineDeploymentTopology replicas value. +func (m *MachineDeploymentTopologyBuilder) WithReplicas(replicas int32) *MachineDeploymentTopologyBuilder { + m.replicas = &replicas + return m +} + +// WithVariables adds variables used as the MachineDeploymentTopology variables value. +func (m *MachineDeploymentTopologyBuilder) WithVariables(variables ...clusterv1.ClusterVariable) *MachineDeploymentTopologyBuilder { + m.variables = variables + return m +} + +// WithMachineHealthCheck adds MachineHealthCheckTopology used as the MachineHealthCheck value. +func (m *MachineDeploymentTopologyBuilder) WithMachineHealthCheck(mhc *clusterv1.MachineHealthCheckTopology) *MachineDeploymentTopologyBuilder { + m.mhc = mhc + return m +} + +// Build returns a testable MachineDeploymentTopology with any values passed to the builder. +func (m *MachineDeploymentTopologyBuilder) Build() clusterv1.MachineDeploymentTopology { + md := clusterv1.MachineDeploymentTopology{ + Class: m.class, + Name: m.name, + Replicas: m.replicas, + MachineHealthCheck: m.mhc, + } + + if len(m.variables) > 0 { + md.Variables = &clusterv1.MachineDeploymentVariables{ + Overrides: m.variables, + } + } + + return md +} + +// MachinePoolTopologyBuilder holds the values needed to create a testable MachinePoolTopology. +type MachinePoolTopologyBuilder struct { + class string + name string + replicas *int32 + failureDomains []string + variables []clusterv1.ClusterVariable +} + +// MachinePoolTopology returns a builder used to create a testable MachinePoolTopology. +func MachinePoolTopology(name string) *MachinePoolTopologyBuilder { + return &MachinePoolTopologyBuilder{ + name: name, + } +} + +// WithClass adds a class string used as the MachinePoolTopology class. +func (m *MachinePoolTopologyBuilder) WithClass(class string) *MachinePoolTopologyBuilder { + m.class = class + return m +} + +// WithReplicas adds a replicas value used as the MachinePoolTopology replicas value. +func (m *MachinePoolTopologyBuilder) WithReplicas(replicas int32) *MachinePoolTopologyBuilder { + m.replicas = &replicas + return m +} + +// WithFailureDomains adds a failureDomains value used as the MachinePoolTopology failureDomains value. +func (m *MachinePoolTopologyBuilder) WithFailureDomains(failureDomains ...string) *MachinePoolTopologyBuilder { + m.failureDomains = failureDomains + return m +} + +// WithVariables adds variables used as the MachinePoolTopology variables value. +func (m *MachinePoolTopologyBuilder) WithVariables(variables ...clusterv1.ClusterVariable) *MachinePoolTopologyBuilder { + m.variables = variables + return m +} + +// Build returns a testable MachinePoolTopology with any values passed to the builder. +func (m *MachinePoolTopologyBuilder) Build() clusterv1.MachinePoolTopology { + mp := clusterv1.MachinePoolTopology{ + Class: m.class, + Name: m.name, + Replicas: m.replicas, + FailureDomains: m.failureDomains, + } + + if len(m.variables) > 0 { + mp.Variables = &clusterv1.MachinePoolVariables{ + Overrides: m.variables, + } + } + + return mp +} + +// ClusterClassBuilder holds the variables and objects required to build a clusterv1.ClusterClass. +type ClusterClassBuilder struct { + namespace string + name string + infrastructureClusterTemplate *unstructured.Unstructured + controlPlaneMetadata *clusterv1.ObjectMeta + controlPlaneTemplate *unstructured.Unstructured + controlPlaneInfrastructureMachineTemplate *unstructured.Unstructured + controlPlaneMHC *clusterv1.MachineHealthCheckClass + controlPlaneNodeDrainTimeout *metav1.Duration + controlPlaneNodeVolumeDetachTimeout *metav1.Duration + controlPlaneNodeDeletionTimeout *metav1.Duration + controlPlaneNamingStrategy *clusterv1.ControlPlaneClassNamingStrategy + machineDeploymentClasses []clusterv1.MachineDeploymentClass + machinePoolClasses []clusterv1.MachinePoolClass + variables []clusterv1.ClusterClassVariable + statusVariables []clusterv1.ClusterClassStatusVariable + patches []clusterv1.ClusterClassPatch +} + +// ClusterClass returns a ClusterClassBuilder with the given name and namespace. +func ClusterClass(namespace, name string) *ClusterClassBuilder { + return &ClusterClassBuilder{ + namespace: namespace, + name: name, + } +} + +// WithInfrastructureClusterTemplate adds the passed InfrastructureClusterTemplate to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithInfrastructureClusterTemplate(t *unstructured.Unstructured) *ClusterClassBuilder { + c.infrastructureClusterTemplate = t + return c +} + +// WithControlPlaneTemplate adds the passed ControlPlaneTemplate to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneTemplate(t *unstructured.Unstructured) *ClusterClassBuilder { + c.controlPlaneTemplate = t + return c +} + +// WithControlPlaneMetadata adds the given labels and annotations for use with the ControlPlane to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneMetadata(labels, annotations map[string]string) *ClusterClassBuilder { + c.controlPlaneMetadata = &clusterv1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + } + return c +} + +// WithControlPlaneInfrastructureMachineTemplate adds the ControlPlane's InfrastructureMachineTemplate to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneInfrastructureMachineTemplate(t *unstructured.Unstructured) *ClusterClassBuilder { + c.controlPlaneInfrastructureMachineTemplate = t + return c +} + +// WithControlPlaneMachineHealthCheck adds a MachineHealthCheck for the ControlPlane to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneMachineHealthCheck(mhc *clusterv1.MachineHealthCheckClass) *ClusterClassBuilder { + c.controlPlaneMHC = mhc + return c +} + +// WithControlPlaneNodeDrainTimeout adds a NodeDrainTimeout for the ControlPlane to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneNodeDrainTimeout(t *metav1.Duration) *ClusterClassBuilder { + c.controlPlaneNodeDrainTimeout = t + return c +} + +// WithControlPlaneNodeVolumeDetachTimeout adds a NodeVolumeDetachTimeout for the ControlPlane to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneNodeVolumeDetachTimeout(t *metav1.Duration) *ClusterClassBuilder { + c.controlPlaneNodeVolumeDetachTimeout = t + return c +} + +// WithControlPlaneNodeDeletionTimeout adds a NodeDeletionTimeout for the ControlPlane to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneNodeDeletionTimeout(t *metav1.Duration) *ClusterClassBuilder { + c.controlPlaneNodeDeletionTimeout = t + return c +} + +// WithControlPlaneNamingStrategy sets the NamingStrategy for the ControlPlane to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneNamingStrategy(n *clusterv1.ControlPlaneClassNamingStrategy) *ClusterClassBuilder { + c.controlPlaneNamingStrategy = n + return c +} + +// WithVariables adds the Variables to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithVariables(vars ...clusterv1.ClusterClassVariable) *ClusterClassBuilder { + c.variables = vars + return c +} + +// WithStatusVariables adds the ClusterClassStatusVariables to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithStatusVariables(vars ...clusterv1.ClusterClassStatusVariable) *ClusterClassBuilder { + c.statusVariables = vars + return c +} + +// WithPatches adds the patches to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithPatches(patches []clusterv1.ClusterClassPatch) *ClusterClassBuilder { + c.patches = patches + return c +} + +// WithWorkerMachineDeploymentClasses adds the variables and objects needed to create MachineDeploymentTemplates for a ClusterClassBuilder. +func (c *ClusterClassBuilder) WithWorkerMachineDeploymentClasses(mdcs ...clusterv1.MachineDeploymentClass) *ClusterClassBuilder { + if c.machineDeploymentClasses == nil { + c.machineDeploymentClasses = make([]clusterv1.MachineDeploymentClass, 0) + } + c.machineDeploymentClasses = append(c.machineDeploymentClasses, mdcs...) + return c +} + +// WithWorkerMachinePoolClasses adds the variables and objects needed to create MachinePoolTemplates for a ClusterClassBuilder. +func (c *ClusterClassBuilder) WithWorkerMachinePoolClasses(mpcs ...clusterv1.MachinePoolClass) *ClusterClassBuilder { + if c.machinePoolClasses == nil { + c.machinePoolClasses = make([]clusterv1.MachinePoolClass, 0) + } + c.machinePoolClasses = append(c.machinePoolClasses, mpcs...) + return c +} + +// Build takes the objects and variables in the ClusterClass builder and uses them to create a ClusterClass object. +func (c *ClusterClassBuilder) Build() *clusterv1.ClusterClass { + obj := &clusterv1.ClusterClass{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterClass", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: c.name, + Namespace: c.namespace, + }, + Spec: clusterv1.ClusterClassSpec{ + Variables: c.variables, + Patches: c.patches, + }, + Status: clusterv1.ClusterClassStatus{ + Variables: c.statusVariables, + }, + } + if c.infrastructureClusterTemplate != nil { + obj.Spec.Infrastructure = clusterv1.LocalObjectTemplate{ + Ref: objToRef(c.infrastructureClusterTemplate), + } + } + if c.controlPlaneMetadata != nil { + obj.Spec.ControlPlane.Metadata = *c.controlPlaneMetadata + } + if c.controlPlaneTemplate != nil { + obj.Spec.ControlPlane.LocalObjectTemplate = clusterv1.LocalObjectTemplate{ + Ref: objToRef(c.controlPlaneTemplate), + } + } + if c.controlPlaneMHC != nil { + obj.Spec.ControlPlane.MachineHealthCheck = c.controlPlaneMHC + } + if c.controlPlaneNodeDrainTimeout != nil { + obj.Spec.ControlPlane.NodeDrainTimeout = c.controlPlaneNodeDrainTimeout + } + if c.controlPlaneNodeVolumeDetachTimeout != nil { + obj.Spec.ControlPlane.NodeVolumeDetachTimeout = c.controlPlaneNodeVolumeDetachTimeout + } + if c.controlPlaneNodeDeletionTimeout != nil { + obj.Spec.ControlPlane.NodeDeletionTimeout = c.controlPlaneNodeDeletionTimeout + } + if c.controlPlaneInfrastructureMachineTemplate != nil { + obj.Spec.ControlPlane.MachineInfrastructure = &clusterv1.LocalObjectTemplate{ + Ref: objToRef(c.controlPlaneInfrastructureMachineTemplate), + } + } + if c.controlPlaneNamingStrategy != nil { + obj.Spec.ControlPlane.NamingStrategy = c.controlPlaneNamingStrategy + } + + obj.Spec.Workers.MachineDeployments = c.machineDeploymentClasses + obj.Spec.Workers.MachinePools = c.machinePoolClasses + return obj +} + +// MachineDeploymentClassBuilder holds the variables and objects required to build a clusterv1.MachineDeploymentClass. +type MachineDeploymentClassBuilder struct { + class string + infrastructureMachineTemplate *unstructured.Unstructured + bootstrapTemplate *unstructured.Unstructured + labels map[string]string + annotations map[string]string + machineHealthCheckClass *clusterv1.MachineHealthCheckClass + failureDomain *string + nodeDrainTimeout *metav1.Duration + nodeVolumeDetachTimeout *metav1.Duration + nodeDeletionTimeout *metav1.Duration + minReadySeconds *int32 + strategy *clusterv1.MachineDeploymentStrategy + namingStrategy *clusterv1.MachineDeploymentClassNamingStrategy +} + +// MachineDeploymentClass returns a MachineDeploymentClassBuilder with the given name and namespace. +func MachineDeploymentClass(class string) *MachineDeploymentClassBuilder { + return &MachineDeploymentClassBuilder{ + class: class, + } +} + +// WithInfrastructureTemplate registers the passed Unstructured object as the InfrastructureMachineTemplate for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithInfrastructureTemplate(t *unstructured.Unstructured) *MachineDeploymentClassBuilder { + m.infrastructureMachineTemplate = t + return m +} + +// WithBootstrapTemplate registers the passed Unstructured object as the BootstrapTemplate for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithBootstrapTemplate(t *unstructured.Unstructured) *MachineDeploymentClassBuilder { + m.bootstrapTemplate = t + return m +} + +// WithLabels sets the labels for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithLabels(labels map[string]string) *MachineDeploymentClassBuilder { + m.labels = labels + return m +} + +// WithAnnotations sets the annotations for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithAnnotations(annotations map[string]string) *MachineDeploymentClassBuilder { + m.annotations = annotations + return m +} + +// WithMachineHealthCheckClass sets the MachineHealthCheckClass for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithMachineHealthCheckClass(mhc *clusterv1.MachineHealthCheckClass) *MachineDeploymentClassBuilder { + m.machineHealthCheckClass = mhc + return m +} + +// WithFailureDomain sets the FailureDomain for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithFailureDomain(f *string) *MachineDeploymentClassBuilder { + m.failureDomain = f + return m +} + +// WithNodeDrainTimeout sets the NodeDrainTimeout for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithNodeDrainTimeout(t *metav1.Duration) *MachineDeploymentClassBuilder { + m.nodeDrainTimeout = t + return m +} + +// WithNodeVolumeDetachTimeout sets the NodeVolumeDetachTimeout for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithNodeVolumeDetachTimeout(t *metav1.Duration) *MachineDeploymentClassBuilder { + m.nodeVolumeDetachTimeout = t + return m +} + +// WithNodeDeletionTimeout sets the NodeDeletionTimeout for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithNodeDeletionTimeout(t *metav1.Duration) *MachineDeploymentClassBuilder { + m.nodeDeletionTimeout = t + return m +} + +// WithMinReadySeconds sets the MinReadySeconds for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithMinReadySeconds(t *int32) *MachineDeploymentClassBuilder { + m.minReadySeconds = t + return m +} + +// WithStrategy sets the Strategy for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithStrategy(s *clusterv1.MachineDeploymentStrategy) *MachineDeploymentClassBuilder { + m.strategy = s + return m +} + +// WithNamingStrategy sets the NamingStrategy for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithNamingStrategy(n *clusterv1.MachineDeploymentClassNamingStrategy) *MachineDeploymentClassBuilder { + m.namingStrategy = n + return m +} + +// Build creates a full MachineDeploymentClass object with the variables passed to the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) Build() *clusterv1.MachineDeploymentClass { + obj := &clusterv1.MachineDeploymentClass{ + Class: m.class, + Template: clusterv1.MachineDeploymentClassTemplate{ + Metadata: clusterv1.ObjectMeta{ + Labels: m.labels, + Annotations: m.annotations, + }, + }, + } + if m.bootstrapTemplate != nil { + obj.Template.Bootstrap.Ref = objToRef(m.bootstrapTemplate) + } + if m.infrastructureMachineTemplate != nil { + obj.Template.Infrastructure.Ref = objToRef(m.infrastructureMachineTemplate) + } + if m.machineHealthCheckClass != nil { + obj.MachineHealthCheck = m.machineHealthCheckClass + } + if m.failureDomain != nil { + obj.FailureDomain = m.failureDomain + } + if m.nodeDrainTimeout != nil { + obj.NodeDrainTimeout = m.nodeDrainTimeout + } + if m.nodeVolumeDetachTimeout != nil { + obj.NodeVolumeDetachTimeout = m.nodeVolumeDetachTimeout + } + if m.nodeDeletionTimeout != nil { + obj.NodeDeletionTimeout = m.nodeDeletionTimeout + } + if m.minReadySeconds != nil { + obj.MinReadySeconds = m.minReadySeconds + } + if m.strategy != nil { + obj.Strategy = m.strategy + } + if m.namingStrategy != nil { + obj.NamingStrategy = m.namingStrategy + } + return obj +} + +// MachinePoolClassBuilder holds the variables and objects required to build a clusterv1.MachinePoolClass. +type MachinePoolClassBuilder struct { + class string + infrastructureMachinePoolTemplate *unstructured.Unstructured + bootstrapTemplate *unstructured.Unstructured + labels map[string]string + annotations map[string]string + failureDomains []string + nodeDrainTimeout *metav1.Duration + nodeVolumeDetachTimeout *metav1.Duration + nodeDeletionTimeout *metav1.Duration + minReadySeconds *int32 + namingStrategy *clusterv1.MachinePoolClassNamingStrategy +} + +// MachinePoolClass returns a MachinePoolClassBuilder with the given name and namespace. +func MachinePoolClass(class string) *MachinePoolClassBuilder { + return &MachinePoolClassBuilder{ + class: class, + } +} + +// WithInfrastructureTemplate registers the passed Unstructured object as the InfrastructureMachinePoolTemplate for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithInfrastructureTemplate(t *unstructured.Unstructured) *MachinePoolClassBuilder { + m.infrastructureMachinePoolTemplate = t + return m +} + +// WithBootstrapTemplate registers the passed Unstructured object as the BootstrapTemplate for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithBootstrapTemplate(t *unstructured.Unstructured) *MachinePoolClassBuilder { + m.bootstrapTemplate = t + return m +} + +// WithLabels sets the labels for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithLabels(labels map[string]string) *MachinePoolClassBuilder { + m.labels = labels + return m +} + +// WithAnnotations sets the annotations for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithAnnotations(annotations map[string]string) *MachinePoolClassBuilder { + m.annotations = annotations + return m +} + +// WithFailureDomains sets the FailureDomains for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithFailureDomains(failureDomains ...string) *MachinePoolClassBuilder { + m.failureDomains = failureDomains + return m +} + +// WithNodeDrainTimeout sets the NodeDrainTimeout for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithNodeDrainTimeout(t *metav1.Duration) *MachinePoolClassBuilder { + m.nodeDrainTimeout = t + return m +} + +// WithNodeVolumeDetachTimeout sets the NodeVolumeDetachTimeout for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithNodeVolumeDetachTimeout(t *metav1.Duration) *MachinePoolClassBuilder { + m.nodeVolumeDetachTimeout = t + return m +} + +// WithNodeDeletionTimeout sets the NodeDeletionTimeout for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithNodeDeletionTimeout(t *metav1.Duration) *MachinePoolClassBuilder { + m.nodeDeletionTimeout = t + return m +} + +// WithMinReadySeconds sets the MinReadySeconds for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithMinReadySeconds(t *int32) *MachinePoolClassBuilder { + m.minReadySeconds = t + return m +} + +// WithNamingStrategy sets the NamingStrategy for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithNamingStrategy(n *clusterv1.MachinePoolClassNamingStrategy) *MachinePoolClassBuilder { + m.namingStrategy = n + return m +} + +// Build creates a full MachinePoolClass object with the variables passed to the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) Build() *clusterv1.MachinePoolClass { + obj := &clusterv1.MachinePoolClass{ + Class: m.class, + Template: clusterv1.MachinePoolClassTemplate{ + Metadata: clusterv1.ObjectMeta{ + Labels: m.labels, + Annotations: m.annotations, + }, + }, + } + if m.bootstrapTemplate != nil { + obj.Template.Bootstrap.Ref = objToRef(m.bootstrapTemplate) + } + if m.infrastructureMachinePoolTemplate != nil { + obj.Template.Infrastructure.Ref = objToRef(m.infrastructureMachinePoolTemplate) + } + if m.failureDomains != nil { + obj.FailureDomains = m.failureDomains + } + if m.nodeDrainTimeout != nil { + obj.NodeDrainTimeout = m.nodeDrainTimeout + } + if m.nodeVolumeDetachTimeout != nil { + obj.NodeVolumeDetachTimeout = m.nodeVolumeDetachTimeout + } + if m.nodeDeletionTimeout != nil { + obj.NodeDeletionTimeout = m.nodeDeletionTimeout + } + if m.minReadySeconds != nil { + obj.MinReadySeconds = m.minReadySeconds + } + if m.namingStrategy != nil { + obj.NamingStrategy = m.namingStrategy + } + return obj +} + +// InfrastructureMachineTemplateBuilder holds the variables and objects needed to build an InfrastructureMachineTemplate. +type InfrastructureMachineTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// InfrastructureMachineTemplate creates an InfrastructureMachineTemplateBuilder with the given name and namespace. +func InfrastructureMachineTemplate(namespace, name string) *InfrastructureMachineTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(GenericInfrastructureMachineTemplateKind) + // Set the mandatory spec fields for the object. + setSpecFields(obj, map[string]interface{}{"spec.template.spec": map[string]interface{}{}}) + return &InfrastructureMachineTemplateBuilder{ + obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *InfrastructureMachineTemplateBuilder) WithSpecFields(fields map[string]interface{}) *InfrastructureMachineTemplateBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build takes the objects and variables in the InfrastructureMachineTemplateBuilder and generates an unstructured object. +func (i *InfrastructureMachineTemplateBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// TestInfrastructureMachineTemplateBuilder holds the variables and objects needed to build an TestInfrastructureMachineTemplate. +type TestInfrastructureMachineTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// TestInfrastructureMachineTemplate creates an TestInfrastructureMachineTemplateBuilder with the given name and namespace. +func TestInfrastructureMachineTemplate(namespace, name string) *TestInfrastructureMachineTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(TestInfrastructureMachineTemplateKind) + // Set the mandatory spec fields for the object. + if err := unstructured.SetNestedField(obj.Object, map[string]interface{}{}, "spec", "template", "spec"); err != nil { + panic(err) + } + return &TestInfrastructureMachineTemplateBuilder{ + obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec."; the path should correspond to a field defined in the CRD. +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *TestInfrastructureMachineTemplateBuilder) WithSpecFields(fields map[string]interface{}) *TestInfrastructureMachineTemplateBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build takes the objects and variables in the InfrastructureMachineTemplateBuilder and generates an unstructured object. +func (i *TestInfrastructureMachineTemplateBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// InfrastructureMachinePoolTemplateBuilder holds the variables and objects needed to build an InfrastructureMachinePoolTemplate. +type InfrastructureMachinePoolTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// InfrastructureMachinePoolTemplate creates an InfrastructureMachinePoolTemplateBuilder with the given name and namespace. +func InfrastructureMachinePoolTemplate(namespace, name string) *InfrastructureMachinePoolTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(GenericInfrastructureMachinePoolTemplateKind) + // Set the mandatory spec fields for the object. + setSpecFields(obj, map[string]interface{}{"spec.template.spec": map[string]interface{}{}}) + return &InfrastructureMachinePoolTemplateBuilder{ + obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *InfrastructureMachinePoolTemplateBuilder) WithSpecFields(fields map[string]interface{}) *InfrastructureMachinePoolTemplateBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build takes the objects and variables in the InfrastructureMachineTemplateBuilder and generates an unstructured object. +func (i *InfrastructureMachinePoolTemplateBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// TestInfrastructureMachinePoolTemplateBuilder holds the variables and objects needed to build an TestInfrastructureMachinePoolTemplate. +type TestInfrastructureMachinePoolTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// TestInfrastructureMachinePoolTemplate creates an TestInfrastructureMachinePoolTemplateBuilder with the given name and namespace. +func TestInfrastructureMachinePoolTemplate(namespace, name string) *TestInfrastructureMachinePoolTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(TestInfrastructureMachinePoolTemplateKind) + // Set the mandatory spec fields for the object. + if err := unstructured.SetNestedField(obj.Object, map[string]interface{}{}, "spec", "template", "spec"); err != nil { + panic(err) + } + return &TestInfrastructureMachinePoolTemplateBuilder{ + obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec."; the path should correspond to a field defined in the CRD. +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *TestInfrastructureMachinePoolTemplateBuilder) WithSpecFields(fields map[string]interface{}) *TestInfrastructureMachinePoolTemplateBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build takes the objects and variables in the TestInfrastructureMachineTemplateBuilder and generates an unstructured object. +func (i *TestInfrastructureMachinePoolTemplateBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// InfrastructureMachinePoolBuilder holds the variables and objects needed to build an InfrastructureMachinePool. +type InfrastructureMachinePoolBuilder struct { + obj *unstructured.Unstructured +} + +// InfrastructureMachinePool creates an InfrastructureMachinePoolBuilder with the given name and namespace. +func InfrastructureMachinePool(namespace, name string) *InfrastructureMachinePoolBuilder { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(GenericInfrastructureMachinePoolKind) + // Set the mandatory spec fields for the object. + setSpecFields(obj, map[string]interface{}{"spec": map[string]interface{}{}}) + return &InfrastructureMachinePoolBuilder{ + obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *InfrastructureMachinePoolBuilder) WithSpecFields(fields map[string]interface{}) *InfrastructureMachinePoolBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build takes the objects and variables in the InfrastructureMachinePoolBuilder and generates an unstructured object. +func (i *InfrastructureMachinePoolBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// TestInfrastructureMachinePoolBuilder holds the variables and objects needed to build an TestInfrastructureMachinePool. +type TestInfrastructureMachinePoolBuilder struct { + obj *unstructured.Unstructured +} + +// TestInfrastructureMachinePool creates an TestInfrastructureMachinePoolBuilder with the given name and namespace. +func TestInfrastructureMachinePool(namespace, name string) *TestInfrastructureMachinePoolBuilder { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(TestInfrastructureMachinePoolKind) + // Set the mandatory spec fields for the object. + if err := unstructured.SetNestedField(obj.Object, map[string]interface{}{}, "spec"); err != nil { + panic(err) + } + return &TestInfrastructureMachinePoolBuilder{ + obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec."; the path should correspond to a field defined in the CRD. +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *TestInfrastructureMachinePoolBuilder) WithSpecFields(fields map[string]interface{}) *TestInfrastructureMachinePoolBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build takes the objects and variables in the TestInfrastructureMachinePoolBuilder and generates an unstructured object. +func (i *TestInfrastructureMachinePoolBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// BootstrapTemplateBuilder holds the variables needed to build a generic BootstrapTemplate. +type BootstrapTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// BootstrapTemplate creates a BootstrapTemplateBuilder with the given name and namespace. +func BootstrapTemplate(namespace, name string) *BootstrapTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(BootstrapGroupVersion.String()) + obj.SetKind(GenericBootstrapConfigTemplateKind) + obj.SetNamespace(namespace) + obj.SetName(name) + setSpecFields(obj, map[string]interface{}{"spec.template.spec": map[string]interface{}{}}) + + return &BootstrapTemplateBuilder{obj: obj} +} + +// WithSpecFields will add fields of any type to the object spec. It takes an argument, fields, which is of the form path: object. +func (b *BootstrapTemplateBuilder) WithSpecFields(fields map[string]interface{}) *BootstrapTemplateBuilder { + setSpecFields(b.obj, fields) + return b +} + +// Build creates a new Unstructured object with the information passed to the BootstrapTemplateBuilder. +func (b *BootstrapTemplateBuilder) Build() *unstructured.Unstructured { + return b.obj +} + +// TestBootstrapTemplateBuilder holds the variables needed to build a generic TestBootstrapTemplate. +type TestBootstrapTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// TestBootstrapTemplate creates a TestBootstrapTemplateBuilder with the given name and namespace. +func TestBootstrapTemplate(namespace, name string) *TestBootstrapTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(BootstrapGroupVersion.String()) + obj.SetKind(TestBootstrapConfigTemplateKind) + obj.SetNamespace(namespace) + obj.SetName(name) + setSpecFields(obj, map[string]interface{}{"spec.template.spec": map[string]interface{}{}}) + + return &TestBootstrapTemplateBuilder{ + obj: obj, + } +} + +// WithSpecFields will add fields of any type to the object spec. It takes an argument, fields, which is of the form path: object. +// NOTE: The path should correspond to a field defined in the CRD. +func (b *TestBootstrapTemplateBuilder) WithSpecFields(fields map[string]interface{}) *TestBootstrapTemplateBuilder { + setSpecFields(b.obj, fields) + return b +} + +// Build creates a new Unstructured object with the information passed to the BootstrapTemplateBuilder. +func (b *TestBootstrapTemplateBuilder) Build() *unstructured.Unstructured { + return b.obj +} + +// BootstrapConfigBuilder holds the variables needed to build a generic BootstrapConfig. +type BootstrapConfigBuilder struct { + obj *unstructured.Unstructured +} + +// BootstrapConfig creates a BootstrapConfigBuilder with the given name and namespace. +func BootstrapConfig(namespace, name string) *BootstrapConfigBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(BootstrapGroupVersion.String()) + obj.SetKind(GenericBootstrapConfigKind) + obj.SetNamespace(namespace) + obj.SetName(name) + setSpecFields(obj, map[string]interface{}{"spec": map[string]interface{}{}}) + + return &BootstrapConfigBuilder{obj: obj} +} + +// WithSpecFields will add fields of any type to the object spec. It takes an argument, fields, which is of the form path: object. +func (b *BootstrapConfigBuilder) WithSpecFields(fields map[string]interface{}) *BootstrapConfigBuilder { + setSpecFields(b.obj, fields) + return b +} + +// Build creates a new Unstructured object with the information passed to the BootstrapConfigBuilder. +func (b *BootstrapConfigBuilder) Build() *unstructured.Unstructured { + return b.obj +} + +// TestBootstrapConfigBuilder holds the variables needed to build a generic TestBootstrapConfig. +type TestBootstrapConfigBuilder struct { + obj *unstructured.Unstructured +} + +// TestBootstrapConfig creates a TestBootstrapConfigBuilder with the given name and namespace. +func TestBootstrapConfig(namespace, name string) *TestBootstrapConfigBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(BootstrapGroupVersion.String()) + obj.SetKind(TestBootstrapConfigKind) + obj.SetNamespace(namespace) + obj.SetName(name) + setSpecFields(obj, map[string]interface{}{"spec": map[string]interface{}{}}) + + return &TestBootstrapConfigBuilder{ + obj: obj, + } +} + +// WithSpecFields will add fields of any type to the object spec. It takes an argument, fields, which is of the form path: object. +// NOTE: The path should correspond to a field defined in the CRD. +func (b *TestBootstrapConfigBuilder) WithSpecFields(fields map[string]interface{}) *TestBootstrapConfigBuilder { + setSpecFields(b.obj, fields) + return b +} + +// Build creates a new Unstructured object with the information passed to the BootstrapConfigBuilder. +func (b *TestBootstrapConfigBuilder) Build() *unstructured.Unstructured { + return b.obj +} + +// InfrastructureClusterTemplateBuilder holds the variables needed to build a generic InfrastructureClusterTemplate. +type InfrastructureClusterTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// InfrastructureClusterTemplate returns an InfrastructureClusterTemplateBuilder with the given name and namespace. +func InfrastructureClusterTemplate(namespace, name string) *InfrastructureClusterTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(GenericInfrastructureClusterTemplateKind) + obj.SetNamespace(namespace) + obj.SetName(name) + if err := unstructured.SetNestedField(obj.Object, map[string]interface{}{}, "spec", "template", "spec"); err != nil { + panic(err) + } + return &InfrastructureClusterTemplateBuilder{ + obj: obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *InfrastructureClusterTemplateBuilder) WithSpecFields(fields map[string]interface{}) *InfrastructureClusterTemplateBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build creates a new Unstructured object with the variables passed to the InfrastructureClusterTemplateBuilder. +func (i *InfrastructureClusterTemplateBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// TestInfrastructureClusterTemplateBuilder holds the variables needed to build a generic TestInfrastructureClusterTemplate. +type TestInfrastructureClusterTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// TestInfrastructureClusterTemplate returns an TestInfrastructureClusterTemplateBuilder with the given name and namespace. +func TestInfrastructureClusterTemplate(namespace, name string) *TestInfrastructureClusterTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(TestInfrastructureClusterTemplateKind) + obj.SetNamespace(namespace) + obj.SetName(name) + if err := unstructured.SetNestedField(obj.Object, map[string]interface{}{}, "spec", "template", "spec"); err != nil { + panic(err) + } + return &TestInfrastructureClusterTemplateBuilder{ + obj: obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec."; the path should correspond to a field defined in the CRD. +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *TestInfrastructureClusterTemplateBuilder) WithSpecFields(fields map[string]interface{}) *TestInfrastructureClusterTemplateBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build creates a new Unstructured object with the variables passed to the InfrastructureClusterTemplateBuilder. +func (i *TestInfrastructureClusterTemplateBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// ControlPlaneTemplateBuilder holds the variables and objects needed to build a generic ControlPlane template. +type ControlPlaneTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// ControlPlaneTemplate creates a NewControlPlaneTemplate builder with the given name and namespace. +func ControlPlaneTemplate(namespace, name string) *ControlPlaneTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(ControlPlaneGroupVersion.String()) + obj.SetKind(GenericControlPlaneTemplateKind) + obj.SetNamespace(namespace) + obj.SetName(name) + + // Initialize the spec.template.spec to make the object valid in reconciliation. + setSpecFields(obj, map[string]interface{}{"spec.template.spec": map[string]interface{}{}}) + return &ControlPlaneTemplateBuilder{obj: obj} +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (c *ControlPlaneTemplateBuilder) WithSpecFields(fields map[string]interface{}) *ControlPlaneTemplateBuilder { + setSpecFields(c.obj, fields) + return c +} + +// WithInfrastructureMachineTemplate adds the given Unstructured object to the ControlPlaneTemplateBuilder as its InfrastructureMachineTemplate. +func (c *ControlPlaneTemplateBuilder) WithInfrastructureMachineTemplate(t *unstructured.Unstructured) *ControlPlaneTemplateBuilder { + if err := setNestedRef(c.obj, t, "spec", "template", "spec", "machineTemplate", "infrastructureRef"); err != nil { + panic(err) + } + return c +} + +// Build creates an Unstructured object from the variables passed to the ControlPlaneTemplateBuilder. +func (c *ControlPlaneTemplateBuilder) Build() *unstructured.Unstructured { + return c.obj +} + +// TestControlPlaneTemplateBuilder holds the variables and objects needed to build a generic ControlPlane template. +type TestControlPlaneTemplateBuilder struct { + obj *unstructured.Unstructured +} + +// TestControlPlaneTemplate creates a NewControlPlaneTemplate builder with the given name and namespace. +func TestControlPlaneTemplate(namespace, name string) *TestControlPlaneTemplateBuilder { + obj := &unstructured.Unstructured{} + obj.SetName(name) + obj.SetNamespace(namespace) + obj.SetAPIVersion(ControlPlaneGroupVersion.String()) + obj.SetKind(TestControlPlaneTemplateKind) + // Set the mandatory spec field for the object. + if err := unstructured.SetNestedField(obj.Object, map[string]interface{}{}, "spec", "template", "spec"); err != nil { + panic(err) + } + return &TestControlPlaneTemplateBuilder{ + obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec."; the path should correspond to a field defined in the CRD. +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (c *TestControlPlaneTemplateBuilder) WithSpecFields(fields map[string]interface{}) *TestControlPlaneTemplateBuilder { + setSpecFields(c.obj, fields) + return c +} + +// WithInfrastructureMachineTemplate adds the given Unstructured object to the ControlPlaneTemplateBuilder as its InfrastructureMachineTemplate. +func (c *TestControlPlaneTemplateBuilder) WithInfrastructureMachineTemplate(t *unstructured.Unstructured) *TestControlPlaneTemplateBuilder { + if err := setNestedRef(c.obj, t, "spec", "template", "spec", "machineTemplate", "infrastructureRef"); err != nil { + panic(err) + } + return c +} + +// Build creates an Unstructured object from the variables passed to the ControlPlaneTemplateBuilder. +func (c *TestControlPlaneTemplateBuilder) Build() *unstructured.Unstructured { + return c.obj +} + +// InfrastructureClusterBuilder holds the variables and objects needed to build a generic InfrastructureCluster. +type InfrastructureClusterBuilder struct { + obj *unstructured.Unstructured +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *InfrastructureClusterBuilder) WithSpecFields(fields map[string]interface{}) *InfrastructureClusterBuilder { + setSpecFields(i.obj, fields) + return i +} + +// InfrastructureCluster returns and InfrastructureClusterBuilder with the given name and namespace. +func InfrastructureCluster(namespace, name string) *InfrastructureClusterBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(GenericInfrastructureClusterKind) + obj.SetNamespace(namespace) + obj.SetName(name) + return &InfrastructureClusterBuilder{obj: obj} +} + +// Build returns an Unstructured object with the information passed to the InfrastructureClusterBuilder. +func (i *InfrastructureClusterBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// TestInfrastructureClusterBuilder holds the variables and objects needed to build a generic TestInfrastructureCluster. +type TestInfrastructureClusterBuilder struct { + obj *unstructured.Unstructured +} + +// TestInfrastructureCluster returns and TestInfrastructureClusterBuilder with the given name and namespace. +func TestInfrastructureCluster(namespace, name string) *TestInfrastructureClusterBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(InfrastructureGroupVersion.String()) + obj.SetKind(TestInfrastructureClusterKind) + obj.SetNamespace(namespace) + obj.SetName(name) + return &TestInfrastructureClusterBuilder{ + obj: obj, + } +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec."; the path should correspond to a field defined in the CRD. +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (i *TestInfrastructureClusterBuilder) WithSpecFields(fields map[string]interface{}) *TestInfrastructureClusterBuilder { + setSpecFields(i.obj, fields) + return i +} + +// Build returns an Unstructured object with the information passed to the InfrastructureClusterBuilder. +func (i *TestInfrastructureClusterBuilder) Build() *unstructured.Unstructured { + return i.obj +} + +// ControlPlaneBuilder holds the variables and objects needed to build a generic object for cluster.spec.controlPlaneRef. +type ControlPlaneBuilder struct { + obj *unstructured.Unstructured +} + +// ControlPlane returns a ControlPlaneBuilder with the given name and Namespace. +func ControlPlane(namespace, name string) *ControlPlaneBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(ControlPlaneGroupVersion.String()) + obj.SetKind(GenericControlPlaneKind) + obj.SetNamespace(namespace) + obj.SetName(name) + return &ControlPlaneBuilder{ + obj: obj, + } +} + +// WithInfrastructureMachineTemplate adds the given unstructured object to the ControlPlaneBuilder as its InfrastructureMachineTemplate. +func (c *ControlPlaneBuilder) WithInfrastructureMachineTemplate(t *unstructured.Unstructured) *ControlPlaneBuilder { + // TODO(killianmuldoon): Update to use the internal/contract package, when it is importable from here + if err := setNestedRef(c.obj, t, "spec", "machineTemplate", "infrastructureRef"); err != nil { + panic(err) + } + return c +} + +// WithReplicas sets the number of replicas for the ControlPlaneBuilder. +func (c *ControlPlaneBuilder) WithReplicas(replicas int64) *ControlPlaneBuilder { + if err := unstructured.SetNestedField(c.obj.Object, replicas, "spec", "replicas"); err != nil { + panic(err) + } + return c +} + +// WithVersion adds the passed version to the ControlPlaneBuilder. +func (c *ControlPlaneBuilder) WithVersion(version string) *ControlPlaneBuilder { + if err := unstructured.SetNestedField(c.obj.Object, version, "spec", "version"); err != nil { + panic(err) + } + return c +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (c *ControlPlaneBuilder) WithSpecFields(fields map[string]interface{}) *ControlPlaneBuilder { + setSpecFields(c.obj, fields) + return c +} + +// WithStatusFields sets a map of status fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the status field. +// +// Note: all the paths should start with "status." +// +// Example map: map[string]interface{}{ +// "status.version": "v1.2.3", +// }. +func (c *ControlPlaneBuilder) WithStatusFields(fields map[string]interface{}) *ControlPlaneBuilder { + setStatusFields(c.obj, fields) + return c +} + +// Build generates an Unstructured object from the information passed to the ControlPlaneBuilder. +func (c *ControlPlaneBuilder) Build() *unstructured.Unstructured { + return c.obj +} + +// TestControlPlaneBuilder holds the variables and objects needed to build a generic object for cluster.spec.controlPlaneRef. +type TestControlPlaneBuilder struct { + obj *unstructured.Unstructured +} + +// TestControlPlane returns a TestControlPlaneBuilder with the given name and Namespace. +func TestControlPlane(namespace, name string) *ControlPlaneBuilder { + obj := &unstructured.Unstructured{} + obj.SetAPIVersion(ControlPlaneGroupVersion.String()) + obj.SetKind(TestControlPlaneKind) + obj.SetNamespace(namespace) + obj.SetName(name) + return &ControlPlaneBuilder{ + obj: obj, + } +} + +// WithInfrastructureMachineTemplate adds the given unstructured object to the TestControlPlaneBuilder as its InfrastructureMachineTemplate. +func (c *TestControlPlaneBuilder) WithInfrastructureMachineTemplate(t *unstructured.Unstructured) *TestControlPlaneBuilder { + // TODO(killianmuldoon): Update to use the internal/contract package, when it is importable from here + if err := setNestedRef(c.obj, t, "spec", "machineTemplate", "infrastructureRef"); err != nil { + panic(err) + } + return c +} + +// WithReplicas sets the number of replicas for the TestControlPlaneBuilder. +func (c *TestControlPlaneBuilder) WithReplicas(replicas int64) *TestControlPlaneBuilder { + if err := unstructured.SetNestedField(c.obj.Object, replicas, "spec", "replicas"); err != nil { + panic(err) + } + return c +} + +// WithVersion adds the passed version to the TestControlPlaneBuilder. +func (c *TestControlPlaneBuilder) WithVersion(version string) *TestControlPlaneBuilder { + if err := unstructured.SetNestedField(c.obj.Object, version, "spec", "version"); err != nil { + panic(err) + } + return c +} + +// WithSpecFields sets a map of spec fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the spec field. +// +// Note: all the paths should start with "spec." +// +// Example map: map[string]interface{}{ +// "spec.version": "v1.2.3", +// }. +func (c *TestControlPlaneBuilder) WithSpecFields(fields map[string]interface{}) *TestControlPlaneBuilder { + setSpecFields(c.obj, fields) + return c +} + +// WithStatusFields sets a map of status fields on the unstructured object. The keys in the map represent the path and the value corresponds +// to the value of the status field. +// +// Note: all the paths should start with "status." +// +// Example map: map[string]interface{}{ +// "status.version": "v1.2.3", +// }. +func (c *TestControlPlaneBuilder) WithStatusFields(fields map[string]interface{}) *TestControlPlaneBuilder { + setStatusFields(c.obj, fields) + return c +} + +// Build generates an Unstructured object from the information passed to the TestControlPlaneBuilder. +func (c *TestControlPlaneBuilder) Build() *unstructured.Unstructured { + return c.obj +} + +// MachinePoolBuilder holds the variables and objects needed to build a generic MachinePool. +type MachinePoolBuilder struct { + namespace string + name string + bootstrap *unstructured.Unstructured + infrastructure *unstructured.Unstructured + version *string + clusterName string + replicas *int32 + labels map[string]string + status *expv1.MachinePoolStatus + minReadySeconds *int32 +} + +// MachinePool creates a MachinePoolBuilder with the given name and namespace. +func MachinePool(namespace, name string) *MachinePoolBuilder { + return &MachinePoolBuilder{ + name: name, + namespace: namespace, + } +} + +// WithBootstrap adds the passed Unstructured object to the MachinePoolBuilder as a bootstrap. +func (m *MachinePoolBuilder) WithBootstrap(ref *unstructured.Unstructured) *MachinePoolBuilder { + m.bootstrap = ref + return m +} + +// WithInfrastructure adds the passed Unstructured object to the MachinePool builder as an InfrastructureMachinePool. +func (m *MachinePoolBuilder) WithInfrastructure(ref *unstructured.Unstructured) *MachinePoolBuilder { + m.infrastructure = ref + return m +} + +// WithLabels adds the given labels to the MachinePoolBuilder. +func (m *MachinePoolBuilder) WithLabels(labels map[string]string) *MachinePoolBuilder { + m.labels = labels + return m +} + +// WithVersion sets the passed version on the MachinePool spec. +func (m *MachinePoolBuilder) WithVersion(version string) *MachinePoolBuilder { + m.version = &version + return m +} + +// WithClusterName sets the passed clusterName on the MachinePool spec. +func (m *MachinePoolBuilder) WithClusterName(clusterName string) *MachinePoolBuilder { + m.clusterName = clusterName + return m +} + +// WithReplicas sets the number of replicas for the MachinePoolBuilder. +func (m *MachinePoolBuilder) WithReplicas(replicas int32) *MachinePoolBuilder { + m.replicas = &replicas + return m +} + +// WithStatus sets the passed status object as the status of the MachinePool object. +func (m *MachinePoolBuilder) WithStatus(status expv1.MachinePoolStatus) *MachinePoolBuilder { + m.status = &status + return m +} + +// WithMinReadySeconds sets the passed value on the machine pool spec. +func (m *MachinePoolBuilder) WithMinReadySeconds(minReadySeconds int32) *MachinePoolBuilder { + m.minReadySeconds = &minReadySeconds + return m +} + +// Build creates a new MachinePool with the variables and objects passed to the MachinePoolBuilder. +func (m *MachinePoolBuilder) Build() *expv1.MachinePool { + obj := &expv1.MachinePool{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachinePool", + APIVersion: expv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.name, + Namespace: m.namespace, + Labels: m.labels, + }, + Spec: expv1.MachinePoolSpec{ + ClusterName: m.clusterName, + Replicas: m.replicas, + MinReadySeconds: m.minReadySeconds, + Template: clusterv1.MachineTemplateSpec{ + Spec: clusterv1.MachineSpec{ + Version: m.version, + ClusterName: m.clusterName, + }, + }, + }, + } + if m.bootstrap != nil { + obj.Spec.Template.Spec.Bootstrap.ConfigRef = objToRef(m.bootstrap) + } + if m.infrastructure != nil { + obj.Spec.Template.Spec.InfrastructureRef = *objToRef(m.infrastructure) + } + if m.status != nil { + obj.Status = *m.status + } + return obj +} + +// MachineDeploymentBuilder holds the variables and objects needed to build a generic MachineDeployment. +type MachineDeploymentBuilder struct { + namespace string + name string + clusterName string + bootstrapTemplate *unstructured.Unstructured + infrastructureTemplate *unstructured.Unstructured + selector *metav1.LabelSelector + version *string + replicas *int32 + generation *int64 + labels map[string]string + status *clusterv1.MachineDeploymentStatus + minReadySeconds *int32 +} + +// MachineDeployment creates a MachineDeploymentBuilder with the given name and namespace. +func MachineDeployment(namespace, name string) *MachineDeploymentBuilder { + return &MachineDeploymentBuilder{ + name: name, + namespace: namespace, + } +} + +// WithBootstrapTemplate adds the passed Unstructured object to the MachineDeploymentBuilder as a bootstrapTemplate. +func (m *MachineDeploymentBuilder) WithBootstrapTemplate(ref *unstructured.Unstructured) *MachineDeploymentBuilder { + m.bootstrapTemplate = ref + return m +} + +// WithInfrastructureTemplate adds the passed unstructured object to the MachineDeployment builder as an infrastructureMachineTemplate. +func (m *MachineDeploymentBuilder) WithInfrastructureTemplate(ref *unstructured.Unstructured) *MachineDeploymentBuilder { + m.infrastructureTemplate = ref + return m +} + +// WithSelector adds the passed selector to the MachineDeployment as the selector. +func (m *MachineDeploymentBuilder) WithSelector(selector metav1.LabelSelector) *MachineDeploymentBuilder { + m.selector = &selector + return m +} + +// WithClusterName adds the clusterName to the MachineDeploymentBuilder. +func (m *MachineDeploymentBuilder) WithClusterName(name string) *MachineDeploymentBuilder { + m.clusterName = name + return m +} + +// WithLabels adds the given labels to the MachineDeploymentBuilder. +func (m *MachineDeploymentBuilder) WithLabels(labels map[string]string) *MachineDeploymentBuilder { + m.labels = labels + return m +} + +// WithVersion sets the passed version on the machine deployment spec. +func (m *MachineDeploymentBuilder) WithVersion(version string) *MachineDeploymentBuilder { + m.version = &version + return m +} + +// WithReplicas sets the number of replicas for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentBuilder) WithReplicas(replicas int32) *MachineDeploymentBuilder { + m.replicas = &replicas + return m +} + +// WithGeneration sets the passed value on the machine deployments object metadata. +func (m *MachineDeploymentBuilder) WithGeneration(generation int64) *MachineDeploymentBuilder { + m.generation = &generation + return m +} + +// WithStatus sets the passed status object as the status of the machine deployment object. +func (m *MachineDeploymentBuilder) WithStatus(status clusterv1.MachineDeploymentStatus) *MachineDeploymentBuilder { + m.status = &status + return m +} + +// WithMinReadySeconds sets the passed value on the machine deployment spec. +func (m *MachineDeploymentBuilder) WithMinReadySeconds(minReadySeconds int32) *MachineDeploymentBuilder { + m.minReadySeconds = &minReadySeconds + return m +} + +// Build creates a new MachineDeployment with the variables and objects passed to the MachineDeploymentBuilder. +func (m *MachineDeploymentBuilder) Build() *clusterv1.MachineDeployment { + obj := &clusterv1.MachineDeployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineDeployment", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.name, + Namespace: m.namespace, + Labels: m.labels, + }, + } + if m.generation != nil { + obj.Generation = *m.generation + } + if m.version != nil { + obj.Spec.Template.Spec.Version = m.version + } + obj.Spec.Replicas = m.replicas + if m.bootstrapTemplate != nil { + obj.Spec.Template.Spec.Bootstrap.ConfigRef = objToRef(m.bootstrapTemplate) + } + if m.infrastructureTemplate != nil { + obj.Spec.Template.Spec.InfrastructureRef = *objToRef(m.infrastructureTemplate) + } + if m.selector != nil { + obj.Spec.Selector = *m.selector + } + if m.status != nil { + obj.Status = *m.status + } + if m.clusterName != "" { + obj.Spec.Template.Spec.ClusterName = m.clusterName + obj.Spec.ClusterName = m.clusterName + if obj.Spec.Selector.MatchLabels == nil { + obj.Spec.Selector.MatchLabels = map[string]string{} + } + obj.Spec.Selector.MatchLabels[clusterv1.ClusterNameLabel] = m.clusterName + obj.Spec.Template.Labels = map[string]string{ + clusterv1.ClusterNameLabel: m.clusterName, + } + } + obj.Spec.MinReadySeconds = m.minReadySeconds + + return obj +} + +// MachineSetBuilder holds the variables and objects needed to build a MachineSet. +type MachineSetBuilder struct { + namespace string + name string + bootstrapTemplate *unstructured.Unstructured + infrastructureTemplate *unstructured.Unstructured + replicas *int32 + labels map[string]string + clusterName string + ownerRefs []metav1.OwnerReference +} + +// MachineSet creates a MachineSetBuilder with the given name and namespace. +func MachineSet(namespace, name string) *MachineSetBuilder { + return &MachineSetBuilder{ + name: name, + namespace: namespace, + } +} + +// WithBootstrapTemplate adds the passed Unstructured object to the MachineSetBuilder as a bootstrapTemplate. +func (m *MachineSetBuilder) WithBootstrapTemplate(ref *unstructured.Unstructured) *MachineSetBuilder { + m.bootstrapTemplate = ref + return m +} + +// WithInfrastructureTemplate adds the passed unstructured object to the MachineSetBuilder as an infrastructureMachineTemplate. +func (m *MachineSetBuilder) WithInfrastructureTemplate(ref *unstructured.Unstructured) *MachineSetBuilder { + m.infrastructureTemplate = ref + return m +} + +// WithLabels adds the given labels to the MachineSetBuilder. +func (m *MachineSetBuilder) WithLabels(labels map[string]string) *MachineSetBuilder { + m.labels = labels + return m +} + +// WithReplicas sets the number of replicas for the MachineSetBuilder. +func (m *MachineSetBuilder) WithReplicas(replicas *int32) *MachineSetBuilder { + m.replicas = replicas + return m +} + +// WithClusterName sets the number of replicas for the MachineSetBuilder. +func (m *MachineSetBuilder) WithClusterName(name string) *MachineSetBuilder { + m.clusterName = name + return m +} + +// WithOwnerReferences adds ownerReferences for the MachineSetBuilder. +func (m *MachineSetBuilder) WithOwnerReferences(ownerRefs []metav1.OwnerReference) *MachineSetBuilder { + m.ownerRefs = ownerRefs + return m +} + +// Build creates a new MachineSet with the variables and objects passed to the MachineSetBuilder. +func (m *MachineSetBuilder) Build() *clusterv1.MachineSet { + obj := &clusterv1.MachineSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineSet", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.name, + Namespace: m.namespace, + Labels: m.labels, + OwnerReferences: m.ownerRefs, + }, + } + obj.Spec.ClusterName = m.clusterName + obj.Spec.Replicas = m.replicas + if m.bootstrapTemplate != nil { + obj.Spec.Template.Spec.Bootstrap.ConfigRef = objToRef(m.bootstrapTemplate) + } + if m.infrastructureTemplate != nil { + obj.Spec.Template.Spec.InfrastructureRef = *objToRef(m.infrastructureTemplate) + } + return obj +} + +// MachineBuilder holds the variables required to build a Machine. +type MachineBuilder struct { + name string + namespace string + version *string + clusterName string + bootstrap *unstructured.Unstructured + labels map[string]string +} + +// Machine returns a MachineBuilder. +func Machine(namespace, name string) *MachineBuilder { + return &MachineBuilder{ + name: name, + namespace: namespace, + } +} + +// WithVersion adds a version to the MachineBuilder. +func (m *MachineBuilder) WithVersion(version string) *MachineBuilder { + m.version = &version + return m +} + +// WithBootstrapTemplate adds a bootstrap template to the MachineBuilder. +func (m *MachineBuilder) WithBootstrapTemplate(bootstrap *unstructured.Unstructured) *MachineBuilder { + m.bootstrap = bootstrap + return m +} + +// WithClusterName adds a clusterName to the MachineBuilder. +func (m *MachineBuilder) WithClusterName(clusterName string) *MachineBuilder { + m.clusterName = clusterName + return m +} + +// WithLabels adds the given labels to the MachineSetBuilder. +func (m *MachineBuilder) WithLabels(labels map[string]string) *MachineBuilder { + m.labels = labels + return m +} + +// Build produces a Machine object from the information passed to the MachineBuilder. +func (m *MachineBuilder) Build() *clusterv1.Machine { + machine := &clusterv1.Machine{ + TypeMeta: metav1.TypeMeta{ + Kind: "Machine", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: m.namespace, + Name: m.name, + Labels: m.labels, + }, + Spec: clusterv1.MachineSpec{ + Version: m.version, + ClusterName: m.clusterName, + }, + } + if m.bootstrap != nil { + machine.Spec.Bootstrap.ConfigRef = objToRef(m.bootstrap) + } + if m.clusterName != "" { + if len(m.labels) == 0 { + machine.Labels = map[string]string{} + } + machine.ObjectMeta.Labels[clusterv1.ClusterNameLabel] = m.clusterName + } + return machine +} + +// objToRef returns a reference to the given object. +func objToRef(obj client.Object) *corev1.ObjectReference { + gvk := obj.GetObjectKind().GroupVersionKind() + return &corev1.ObjectReference{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } +} + +// setNestedRef sets the value of a nested field to a reference to the refObj provided. +func setNestedRef(obj, refObj *unstructured.Unstructured, fields ...string) error { + ref := map[string]interface{}{ + "kind": refObj.GetKind(), + "namespace": refObj.GetNamespace(), + "name": refObj.GetName(), + "apiVersion": refObj.GetAPIVersion(), + } + return unstructured.SetNestedField(obj.UnstructuredContent(), ref, fields...) +} + +// setSpecFields sets fields in an unstructured object from a map. +func setSpecFields(obj *unstructured.Unstructured, fields map[string]interface{}) { + for k, v := range fields { + fieldParts := strings.Split(k, ".") + if len(fieldParts) == 0 { + panic(fmt.Errorf("fieldParts invalid")) + } + if fieldParts[0] != "spec" { + panic(fmt.Errorf("can not set fields outside spec")) + } + if err := unstructured.SetNestedField(obj.UnstructuredContent(), v, strings.Split(k, ".")...); err != nil { + panic(err) + } + } +} + +// setStatusFields sets fields in an unstructured object from a map. +func setStatusFields(obj *unstructured.Unstructured, fields map[string]interface{}) { + for k, v := range fields { + fieldParts := strings.Split(k, ".") + if len(fieldParts) == 0 { + panic(fmt.Errorf("fieldParts invalid")) + } + if fieldParts[0] != "status" { + panic(fmt.Errorf("can not set fields outside status")) + } + if err := unstructured.SetNestedField(obj.UnstructuredContent(), v, strings.Split(k, ".")...); err != nil { + panic(err) + } + } +} + +// MachineHealthCheckBuilder holds fields for creating a MachineHealthCheck. +type MachineHealthCheckBuilder struct { + name string + namespace string + ownerRefs []metav1.OwnerReference + selector metav1.LabelSelector + clusterName string + conditions []clusterv1.UnhealthyCondition + maxUnhealthy *intstr.IntOrString +} + +// MachineHealthCheck returns a MachineHealthCheckBuilder with the given name and namespace. +func MachineHealthCheck(namespace, name string) *MachineHealthCheckBuilder { + return &MachineHealthCheckBuilder{ + name: name, + namespace: namespace, + } +} + +// WithSelector adds the selector used to target machines for the MachineHealthCheck. +func (m *MachineHealthCheckBuilder) WithSelector(selector metav1.LabelSelector) *MachineHealthCheckBuilder { + m.selector = selector + return m +} + +// WithClusterName adds a cluster name for the MachineHealthCheck. +func (m *MachineHealthCheckBuilder) WithClusterName(clusterName string) *MachineHealthCheckBuilder { + m.clusterName = clusterName + return m +} + +// WithUnhealthyConditions adds the spec used to build the parameters of the MachineHealthCheck. +func (m *MachineHealthCheckBuilder) WithUnhealthyConditions(conditions []clusterv1.UnhealthyCondition) *MachineHealthCheckBuilder { + m.conditions = conditions + return m +} + +// WithOwnerReferences adds ownerreferences for the MachineHealthCheck. +func (m *MachineHealthCheckBuilder) WithOwnerReferences(ownerRefs []metav1.OwnerReference) *MachineHealthCheckBuilder { + m.ownerRefs = ownerRefs + return m +} + +// WithMaxUnhealthy adds a MaxUnhealthyValue for the MachineHealthCheck. +func (m *MachineHealthCheckBuilder) WithMaxUnhealthy(maxUnhealthy *intstr.IntOrString) *MachineHealthCheckBuilder { + m.maxUnhealthy = maxUnhealthy + return m +} + +// Build returns a MachineHealthCheck with the supplied details. +func (m *MachineHealthCheckBuilder) Build() *clusterv1.MachineHealthCheck { + // create a MachineHealthCheck with the spec given in the ClusterClass + mhc := &clusterv1.MachineHealthCheck{ + TypeMeta: metav1.TypeMeta{ + Kind: "MachineHealthCheck", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.name, + Namespace: m.namespace, + OwnerReferences: m.ownerRefs, + }, + Spec: clusterv1.MachineHealthCheckSpec{ + ClusterName: m.clusterName, + Selector: m.selector, + UnhealthyConditions: m.conditions, + MaxUnhealthy: m.maxUnhealthy, + }, + } + if m.clusterName != "" { + mhc.Labels = map[string]string{clusterv1.ClusterNameLabel: m.clusterName} + } + + return mhc +} diff --git a/internal/test/builder/controlplane.go b/internal/test/builder/controlplane.go new file mode 100644 index 000000000..e715fcd02 --- /dev/null +++ b/internal/test/builder/controlplane.go @@ -0,0 +1,152 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // ControlPlaneGroupVersion is group version used for control plane objects. + ControlPlaneGroupVersion = schema.GroupVersion{Group: "controlplane.cluster.x-k8s.io", Version: "v1beta1"} + + // GenericControlPlaneKind is the Kind for the GenericControlPlane. + GenericControlPlaneKind = "GenericControlPlane" + // GenericControlPlaneCRD is a generic control plane CRD. + GenericControlPlaneCRD = untypedCRD(ControlPlaneGroupVersion.WithKind(GenericControlPlaneKind)) + + // GenericControlPlaneTemplateKind is the Kind for the GenericControlPlaneTemplate. + GenericControlPlaneTemplateKind = "GenericControlPlaneTemplate" + // GenericControlPlaneTemplateCRD is a generic control plane template CRD. + GenericControlPlaneTemplateCRD = untypedCRD(ControlPlaneGroupVersion.WithKind(GenericControlPlaneTemplateKind)) + + // TODO: drop generic CRDs in favour of typed test CRDs. + + // TestControlPlaneTemplateKind is the Kind for the TestControlPlaneTemplate. + TestControlPlaneTemplateKind = "TestControlPlaneTemplate" + // TestControlPlaneTemplateCRD is a test control plane template CRD. + TestControlPlaneTemplateCRD = testControlPlaneTemplateCRD(ControlPlaneGroupVersion.WithKind(TestControlPlaneTemplateKind)) + + // TestControlPlaneKind is the Kind for the TestControlPlane. + TestControlPlaneKind = "TestControlPlane" + // TestControlPlaneCRD is a test control plane CRD. + TestControlPlaneCRD = testControlPlaneCRD(ControlPlaneGroupVersion.WithKind(TestControlPlaneKind)) +) + +func testControlPlaneTemplateCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "template": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "spec": controPlaneSpecSchema, + }, + }, + }, + }, + }) +} + +func testControlPlaneCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": controPlaneSpecSchema, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // mandatory field from the Cluster API contract + "ready": {Type: "boolean"}, + // mandatory field from the Cluster API contract - replicas support + "replicas": {Type: "integer", Format: "int32"}, + "selector": {Type: "string"}, + "readyReplicas": {Type: "integer", Format: "int32"}, + "unavailableReplicas": {Type: "integer", Format: "int32"}, + "updatedReplicas": {Type: "integer", Format: "int32"}, + // Mandatory field from the Cluster API contract - version support + "version": {Type: "string"}, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + }, + }) +} + +var ( + controPlaneSpecSchema = apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract - version support + "version": { + Type: "string", + }, + // mandatory field from the Cluster API contract - replicas support + "replicas": { + Type: "integer", + Format: "int32", + }, + // mandatory field from the Cluster API contract - using Machines support + "machineTemplate": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": metadataSchema, + "infrastructureRef": refSchema, + "nodeDeletionTimeout": {Type: "string"}, + "nodeDrainTimeout": {Type: "string"}, + }, + }, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + // Copy of a subset of KCP spec fields to test server side apply on deep nested structs + "kubeadmConfigSpec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "clusterConfiguration": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "controllerManager": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "extraArgs": { + Type: "object", + AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{ + Schema: &apiextensionsv1.JSONSchemaProps{Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +) diff --git a/internal/test/builder/crds.go b/internal/test/builder/crds.go new file mode 100644 index 000000000..67caa0e29 --- /dev/null +++ b/internal/test/builder/crds.go @@ -0,0 +1,113 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + "strings" + + "github.com/gobuffalo/flect" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/contract" +) + +func untypedCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "spec": { + Type: "object", + XPreserveUnknownFields: ptr.To(true), + }, + "status": { + Type: "object", + XPreserveUnknownFields: ptr.To(true), + }, + }) +} + +func generateCRD(gvk schema.GroupVersionKind, properties map[string]apiextensionsv1.JSONSchemaProps) *apiextensionsv1.CustomResourceDefinition { + return &apiextensionsv1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiextensionsv1.SchemeGroupVersion.String(), + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: contract.CalculateCRDName(gvk.Group, gvk.Kind), + Labels: map[string]string{ + clusterv1.GroupVersion.String(): "v1beta1", + }, + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: gvk.Group, + Scope: apiextensionsv1.NamespaceScoped, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Kind: gvk.Kind, + Plural: flect.Pluralize(strings.ToLower(gvk.Kind)), + }, + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: gvk.Version, + Served: true, + Storage: true, + Subresources: &apiextensionsv1.CustomResourceSubresources{ + Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, + }, + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: properties, + }, + }, + }, + }, + }, + } +} + +var ( + refSchema = apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "apiVersion": {Type: "string"}, + "kind": {Type: "string"}, + "name": {Type: "string"}, + "namespace": {Type: "string"}, + // NOTE: omitting fields not used for sake of simplicity. + }, + } + + metadataSchema = apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "labels": { + Type: "object", + AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{ + Schema: &apiextensionsv1.JSONSchemaProps{Type: "string"}, + }, + }, + "annotations": { + Type: "object", + AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{ + Schema: &apiextensionsv1.JSONSchemaProps{Type: "string"}, + }, + }, + }, + } +) diff --git a/internal/test/builder/doc.go b/internal/test/builder/doc.go new file mode 100644 index 000000000..f0c3d46b6 --- /dev/null +++ b/internal/test/builder/doc.go @@ -0,0 +1,24 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package builder implements builder and CRDs for creating API objects for testing. +// +kubebuilder:object:generate=true + +/* +This package includes code from [kubernetes-sigs/cluster-api](https://github.com/kubernetes-sigs/cluster-api), +licensed under the Apache License, Version 2.0. +*/ +package builder diff --git a/internal/test/builder/infrastructure.go b/internal/test/builder/infrastructure.go new file mode 100644 index 000000000..dfcb91775 --- /dev/null +++ b/internal/test/builder/infrastructure.go @@ -0,0 +1,288 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // InfrastructureGroupVersion is group version used for infrastructure objects. + InfrastructureGroupVersion = schema.GroupVersion{Group: "infrastructure.cluster.x-k8s.io", Version: "v1beta1"} + + // GenericInfrastructureMachineKind is the Kind for the GenericInfrastructureMachine. + GenericInfrastructureMachineKind = "GenericInfrastructureMachine" + // GenericInfrastructureMachineCRD is a generic infrastructure machine CRD. + GenericInfrastructureMachineCRD = untypedCRD(InfrastructureGroupVersion.WithKind(GenericInfrastructureMachineKind)) + + // GenericInfrastructureMachineTemplateKind is the Kind for the GenericInfrastructureMachineTemplate. + GenericInfrastructureMachineTemplateKind = "GenericInfrastructureMachineTemplate" + // GenericInfrastructureMachineTemplateCRD is a generic infrastructure machine template CRD. + GenericInfrastructureMachineTemplateCRD = untypedCRD(InfrastructureGroupVersion.WithKind(GenericInfrastructureMachineTemplateKind)) + + // GenericInfrastructureMachinePoolTemplateKind is the Kind for the GenericInfrastructureMachinePoolTemplate. + GenericInfrastructureMachinePoolTemplateKind = "GenericInfrastructureMachinePoolTemplate" + // GenericInfrastructureMachinePoolTemplateCRD is a generic infrastructure machine pool template CRD. + GenericInfrastructureMachinePoolTemplateCRD = untypedCRD(InfrastructureGroupVersion.WithKind(GenericInfrastructureMachinePoolTemplateKind)) + + // GenericInfrastructureMachinePoolKind is the Kind for the GenericInfrastructureMachinePool. + GenericInfrastructureMachinePoolKind = "GenericInfrastructureMachinePool" + // GenericInfrastructureMachinePoolCRD is a generic infrastructure machine pool CRD. + GenericInfrastructureMachinePoolCRD = untypedCRD(InfrastructureGroupVersion.WithKind(GenericInfrastructureMachinePoolKind)) + + // GenericInfrastructureClusterKind is the kind for the GenericInfrastructureCluster type. + GenericInfrastructureClusterKind = "GenericInfrastructureCluster" + // GenericInfrastructureClusterCRD is a generic infrastructure machine CRD. + GenericInfrastructureClusterCRD = untypedCRD(InfrastructureGroupVersion.WithKind(GenericInfrastructureClusterKind)) + + // GenericInfrastructureClusterTemplateKind is the kind for the GenericInfrastructureClusterTemplate type. + GenericInfrastructureClusterTemplateKind = "GenericInfrastructureClusterTemplate" + // GenericInfrastructureClusterTemplateCRD is a generic infrastructure machine template CRD. + GenericInfrastructureClusterTemplateCRD = untypedCRD(InfrastructureGroupVersion.WithKind(GenericInfrastructureClusterTemplateKind)) + + // TODO: drop generic CRDs in favour of typed test CRDs. + + // TestInfrastructureClusterTemplateKind is the kind for the TestInfrastructureClusterTemplate type. + TestInfrastructureClusterTemplateKind = "TestInfrastructureClusterTemplate" + // TestInfrastructureClusterTemplateCRD is a test infrastructure machine template CRD. + TestInfrastructureClusterTemplateCRD = testInfrastructureClusterTemplateCRD(InfrastructureGroupVersion.WithKind(TestInfrastructureClusterTemplateKind)) + + // TestInfrastructureClusterKind is the kind for the TestInfrastructureCluster type. + TestInfrastructureClusterKind = "TestInfrastructureCluster" + // TestInfrastructureClusterCRD is a test infrastructure machine CRD. + TestInfrastructureClusterCRD = testInfrastructureClusterCRD(InfrastructureGroupVersion.WithKind(TestInfrastructureClusterKind)) + + // TestInfrastructureMachineTemplateKind is the kind for the TestInfrastructureMachineTemplate type. + TestInfrastructureMachineTemplateKind = "TestInfrastructureMachineTemplate" + // TestInfrastructureMachineTemplateCRD is a test infrastructure machine template CRD. + TestInfrastructureMachineTemplateCRD = testInfrastructureMachineTemplateCRD(InfrastructureGroupVersion.WithKind(TestInfrastructureMachineTemplateKind)) + + // TestInfrastructureMachinePoolTemplateKind is the kind for the TestInfrastructureMachinePoolTemplate type. + TestInfrastructureMachinePoolTemplateKind = "TestInfrastructureMachinePoolTemplate" + // TestInfrastructureMachinePoolTemplateCRD is a test infrastructure machine pool template CRD. + TestInfrastructureMachinePoolTemplateCRD = testInfrastructureMachinePoolTemplateCRD(InfrastructureGroupVersion.WithKind(TestInfrastructureMachinePoolTemplateKind)) + + // TestInfrastructureMachinePoolKind is the kind for the TestInfrastructureMachinePool type. + TestInfrastructureMachinePoolKind = "TestInfrastructureMachinePool" + // TestInfrastructureMachinePoolCRD is a test infrastructure machine CRD. + TestInfrastructureMachinePoolCRD = testInfrastructureMachinePoolCRD(InfrastructureGroupVersion.WithKind(TestInfrastructureMachinePoolKind)) + + // TestInfrastructureMachineKind is the kind for the TestInfrastructureMachine type. + TestInfrastructureMachineKind = "TestInfrastructureMachine" + // TestInfrastructureMachineCRD is a test infrastructure machine CRD. + TestInfrastructureMachineCRD = testInfrastructureMachineCRD(InfrastructureGroupVersion.WithKind(TestInfrastructureMachineKind)) +) + +func testInfrastructureClusterTemplateCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "template": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "spec": clusterSpecSchema, + }, + }, + }, + }, + }) +} + +func testInfrastructureClusterCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": clusterSpecSchema, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // mandatory field from the Cluster API contract + "ready": {Type: "boolean"}, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + Required: []string{"ready"}, + }, + }) +} + +func testInfrastructureMachineTemplateCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "template": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": metadataSchema, + "spec": machineSpecSchema, + }, + }, + }, + }, + }) +} + +func testInfrastructureMachinePoolTemplateCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "template": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": metadataSchema, + "spec": machinePoolSpecSchema, + }, + }, + }, + }, + }) +} + +func testInfrastructureMachineCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": machineSpecSchema, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // mandatory field from the Cluster API contract + "ready": {Type: "boolean"}, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + }, + }) +} + +func testInfrastructureMachinePoolCRD(gvk schema.GroupVersionKind) *apiextensionsv1.CustomResourceDefinition { + return generateCRD(gvk, map[string]apiextensionsv1.JSONSchemaProps{ + "metadata": { + // NOTE: in CRD there is only a partial definition of metadata schema. + // Ref https://github.com/kubernetes-sigs/controller-tools/blob/59485af1c1f6a664655dad49543c474bb4a0d2a2/pkg/crd/gen.go#L185 + Type: "object", + }, + "spec": machinePoolSpecSchema, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // mandatory field from the Cluster API contract + "ready": {Type: "boolean"}, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + }, + }) +} + +var ( + clusterSpecSchema = apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "controlPlaneEndpoint": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "host": {Type: "string"}, + "port": {Type: "integer"}, + }, + Required: []string{"host", "port"}, + }, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + "fooMap": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "foo": {Type: "string"}, + }, + }, + "fooList": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "foo": {Type: "string"}, + }, + }, + }, + }, + // Field for testing + }, + } + + machineSpecSchema = apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "providerID": {Type: "string"}, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + } + + machinePoolSpecSchema = apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + // Mandatory field from the Cluster API contract + "providerIDList": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + // General purpose fields to be used in different test scenario. + "foo": {Type: "string"}, + "bar": {Type: "string"}, + }, + } +) diff --git a/internal/test/builder/remediation.go b/internal/test/builder/remediation.go new file mode 100644 index 000000000..b27b37528 --- /dev/null +++ b/internal/test/builder/remediation.go @@ -0,0 +1,32 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package builder + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // RemediationGroupVersion is group version used for remediation objects. + RemediationGroupVersion = schema.GroupVersion{Group: "remediation.external.io", Version: "v1beta1"} + + // GenericRemediationCRD is a generic infrastructure remediation CRD. + GenericRemediationCRD = untypedCRD(RemediationGroupVersion.WithKind("GenericExternalRemediation")) + + // GenericRemediationTemplateCRD is a generic infrastructure remediation template CRD. + GenericRemediationTemplateCRD = untypedCRD(RemediationGroupVersion.WithKind("GenericExternalRemediationTemplate")) +) diff --git a/internal/test/builder/zz_generated.deepcopy.go b/internal/test/builder/zz_generated.deepcopy.go new file mode 100644 index 000000000..355224b14 --- /dev/null +++ b/internal/test/builder/zz_generated.deepcopy.go @@ -0,0 +1,965 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package builder + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/cluster-api/api/v1beta1" + apiv1beta1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BootstrapConfigBuilder) DeepCopyInto(out *BootstrapConfigBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapConfigBuilder. +func (in *BootstrapConfigBuilder) DeepCopy() *BootstrapConfigBuilder { + if in == nil { + return nil + } + out := new(BootstrapConfigBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BootstrapTemplateBuilder) DeepCopyInto(out *BootstrapTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapTemplateBuilder. +func (in *BootstrapTemplateBuilder) DeepCopy() *BootstrapTemplateBuilder { + if in == nil { + return nil + } + out := new(BootstrapTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterBuilder) DeepCopyInto(out *ClusterBuilder) { + *out = *in + if in.labels != nil { + in, out := &in.labels, &out.labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.annotations != nil { + in, out := &in.annotations, &out.annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.topology != nil { + in, out := &in.topology, &out.topology + *out = new(v1beta1.Topology) + (*in).DeepCopyInto(*out) + } + if in.infrastructureCluster != nil { + in, out := &in.infrastructureCluster, &out.infrastructureCluster + *out = (*in).DeepCopy() + } + if in.controlPlane != nil { + in, out := &in.controlPlane, &out.controlPlane + *out = (*in).DeepCopy() + } + if in.network != nil { + in, out := &in.network, &out.network + *out = new(v1beta1.ClusterNetwork) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterBuilder. +func (in *ClusterBuilder) DeepCopy() *ClusterBuilder { + if in == nil { + return nil + } + out := new(ClusterBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClassBuilder) DeepCopyInto(out *ClusterClassBuilder) { + *out = *in + if in.infrastructureClusterTemplate != nil { + in, out := &in.infrastructureClusterTemplate, &out.infrastructureClusterTemplate + *out = (*in).DeepCopy() + } + if in.controlPlaneMetadata != nil { + in, out := &in.controlPlaneMetadata, &out.controlPlaneMetadata + *out = new(v1beta1.ObjectMeta) + (*in).DeepCopyInto(*out) + } + if in.controlPlaneTemplate != nil { + in, out := &in.controlPlaneTemplate, &out.controlPlaneTemplate + *out = (*in).DeepCopy() + } + if in.controlPlaneInfrastructureMachineTemplate != nil { + in, out := &in.controlPlaneInfrastructureMachineTemplate, &out.controlPlaneInfrastructureMachineTemplate + *out = (*in).DeepCopy() + } + if in.controlPlaneMHC != nil { + in, out := &in.controlPlaneMHC, &out.controlPlaneMHC + *out = new(v1beta1.MachineHealthCheckClass) + (*in).DeepCopyInto(*out) + } + if in.controlPlaneNodeDrainTimeout != nil { + in, out := &in.controlPlaneNodeDrainTimeout, &out.controlPlaneNodeDrainTimeout + *out = new(v1.Duration) + **out = **in + } + if in.controlPlaneNodeVolumeDetachTimeout != nil { + in, out := &in.controlPlaneNodeVolumeDetachTimeout, &out.controlPlaneNodeVolumeDetachTimeout + *out = new(v1.Duration) + **out = **in + } + if in.controlPlaneNodeDeletionTimeout != nil { + in, out := &in.controlPlaneNodeDeletionTimeout, &out.controlPlaneNodeDeletionTimeout + *out = new(v1.Duration) + **out = **in + } + if in.controlPlaneNamingStrategy != nil { + in, out := &in.controlPlaneNamingStrategy, &out.controlPlaneNamingStrategy + *out = new(v1beta1.ControlPlaneClassNamingStrategy) + (*in).DeepCopyInto(*out) + } + if in.machineDeploymentClasses != nil { + in, out := &in.machineDeploymentClasses, &out.machineDeploymentClasses + *out = make([]v1beta1.MachineDeploymentClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.machinePoolClasses != nil { + in, out := &in.machinePoolClasses, &out.machinePoolClasses + *out = make([]v1beta1.MachinePoolClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.variables != nil { + in, out := &in.variables, &out.variables + *out = make([]v1beta1.ClusterClassVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.statusVariables != nil { + in, out := &in.statusVariables, &out.statusVariables + *out = make([]v1beta1.ClusterClassStatusVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.patches != nil { + in, out := &in.patches, &out.patches + *out = make([]v1beta1.ClusterClassPatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassBuilder. +func (in *ClusterClassBuilder) DeepCopy() *ClusterClassBuilder { + if in == nil { + return nil + } + out := new(ClusterClassBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterTopologyBuilder) DeepCopyInto(out *ClusterTopologyBuilder) { + *out = *in + if in.workers != nil { + in, out := &in.workers, &out.workers + *out = new(v1beta1.WorkersTopology) + (*in).DeepCopyInto(*out) + } + if in.controlPlaneMHC != nil { + in, out := &in.controlPlaneMHC, &out.controlPlaneMHC + *out = new(v1beta1.MachineHealthCheckTopology) + (*in).DeepCopyInto(*out) + } + if in.variables != nil { + in, out := &in.variables, &out.variables + *out = make([]v1beta1.ClusterVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterTopologyBuilder. +func (in *ClusterTopologyBuilder) DeepCopy() *ClusterTopologyBuilder { + if in == nil { + return nil + } + out := new(ClusterTopologyBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneBuilder) DeepCopyInto(out *ControlPlaneBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneBuilder. +func (in *ControlPlaneBuilder) DeepCopy() *ControlPlaneBuilder { + if in == nil { + return nil + } + out := new(ControlPlaneBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneTemplateBuilder) DeepCopyInto(out *ControlPlaneTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneTemplateBuilder. +func (in *ControlPlaneTemplateBuilder) DeepCopy() *ControlPlaneTemplateBuilder { + if in == nil { + return nil + } + out := new(ControlPlaneTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureClusterBuilder) DeepCopyInto(out *InfrastructureClusterBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureClusterBuilder. +func (in *InfrastructureClusterBuilder) DeepCopy() *InfrastructureClusterBuilder { + if in == nil { + return nil + } + out := new(InfrastructureClusterBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureClusterTemplateBuilder) DeepCopyInto(out *InfrastructureClusterTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureClusterTemplateBuilder. +func (in *InfrastructureClusterTemplateBuilder) DeepCopy() *InfrastructureClusterTemplateBuilder { + if in == nil { + return nil + } + out := new(InfrastructureClusterTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureMachinePoolBuilder) DeepCopyInto(out *InfrastructureMachinePoolBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureMachinePoolBuilder. +func (in *InfrastructureMachinePoolBuilder) DeepCopy() *InfrastructureMachinePoolBuilder { + if in == nil { + return nil + } + out := new(InfrastructureMachinePoolBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureMachinePoolTemplateBuilder) DeepCopyInto(out *InfrastructureMachinePoolTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureMachinePoolTemplateBuilder. +func (in *InfrastructureMachinePoolTemplateBuilder) DeepCopy() *InfrastructureMachinePoolTemplateBuilder { + if in == nil { + return nil + } + out := new(InfrastructureMachinePoolTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureMachineTemplateBuilder) DeepCopyInto(out *InfrastructureMachineTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureMachineTemplateBuilder. +func (in *InfrastructureMachineTemplateBuilder) DeepCopy() *InfrastructureMachineTemplateBuilder { + if in == nil { + return nil + } + out := new(InfrastructureMachineTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineBuilder) DeepCopyInto(out *MachineBuilder) { + *out = *in + if in.version != nil { + in, out := &in.version, &out.version + *out = new(string) + **out = **in + } + if in.bootstrap != nil { + in, out := &in.bootstrap, &out.bootstrap + *out = (*in).DeepCopy() + } + if in.labels != nil { + in, out := &in.labels, &out.labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineBuilder. +func (in *MachineBuilder) DeepCopy() *MachineBuilder { + if in == nil { + return nil + } + out := new(MachineBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineDeploymentBuilder) DeepCopyInto(out *MachineDeploymentBuilder) { + *out = *in + if in.bootstrapTemplate != nil { + in, out := &in.bootstrapTemplate, &out.bootstrapTemplate + *out = (*in).DeepCopy() + } + if in.infrastructureTemplate != nil { + in, out := &in.infrastructureTemplate, &out.infrastructureTemplate + *out = (*in).DeepCopy() + } + if in.selector != nil { + in, out := &in.selector, &out.selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.version != nil { + in, out := &in.version, &out.version + *out = new(string) + **out = **in + } + if in.replicas != nil { + in, out := &in.replicas, &out.replicas + *out = new(int32) + **out = **in + } + if in.generation != nil { + in, out := &in.generation, &out.generation + *out = new(int64) + **out = **in + } + if in.labels != nil { + in, out := &in.labels, &out.labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.status != nil { + in, out := &in.status, &out.status + *out = new(v1beta1.MachineDeploymentStatus) + (*in).DeepCopyInto(*out) + } + if in.minReadySeconds != nil { + in, out := &in.minReadySeconds, &out.minReadySeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineDeploymentBuilder. +func (in *MachineDeploymentBuilder) DeepCopy() *MachineDeploymentBuilder { + if in == nil { + return nil + } + out := new(MachineDeploymentBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineDeploymentClassBuilder) DeepCopyInto(out *MachineDeploymentClassBuilder) { + *out = *in + if in.infrastructureMachineTemplate != nil { + in, out := &in.infrastructureMachineTemplate, &out.infrastructureMachineTemplate + *out = (*in).DeepCopy() + } + if in.bootstrapTemplate != nil { + in, out := &in.bootstrapTemplate, &out.bootstrapTemplate + *out = (*in).DeepCopy() + } + if in.labels != nil { + in, out := &in.labels, &out.labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.annotations != nil { + in, out := &in.annotations, &out.annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.machineHealthCheckClass != nil { + in, out := &in.machineHealthCheckClass, &out.machineHealthCheckClass + *out = new(v1beta1.MachineHealthCheckClass) + (*in).DeepCopyInto(*out) + } + if in.failureDomain != nil { + in, out := &in.failureDomain, &out.failureDomain + *out = new(string) + **out = **in + } + if in.nodeDrainTimeout != nil { + in, out := &in.nodeDrainTimeout, &out.nodeDrainTimeout + *out = new(v1.Duration) + **out = **in + } + if in.nodeVolumeDetachTimeout != nil { + in, out := &in.nodeVolumeDetachTimeout, &out.nodeVolumeDetachTimeout + *out = new(v1.Duration) + **out = **in + } + if in.nodeDeletionTimeout != nil { + in, out := &in.nodeDeletionTimeout, &out.nodeDeletionTimeout + *out = new(v1.Duration) + **out = **in + } + if in.minReadySeconds != nil { + in, out := &in.minReadySeconds, &out.minReadySeconds + *out = new(int32) + **out = **in + } + if in.strategy != nil { + in, out := &in.strategy, &out.strategy + *out = new(v1beta1.MachineDeploymentStrategy) + (*in).DeepCopyInto(*out) + } + if in.namingStrategy != nil { + in, out := &in.namingStrategy, &out.namingStrategy + *out = new(v1beta1.MachineDeploymentClassNamingStrategy) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineDeploymentClassBuilder. +func (in *MachineDeploymentClassBuilder) DeepCopy() *MachineDeploymentClassBuilder { + if in == nil { + return nil + } + out := new(MachineDeploymentClassBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineDeploymentTopologyBuilder) DeepCopyInto(out *MachineDeploymentTopologyBuilder) { + *out = *in + if in.replicas != nil { + in, out := &in.replicas, &out.replicas + *out = new(int32) + **out = **in + } + if in.mhc != nil { + in, out := &in.mhc, &out.mhc + *out = new(v1beta1.MachineHealthCheckTopology) + (*in).DeepCopyInto(*out) + } + if in.variables != nil { + in, out := &in.variables, &out.variables + *out = make([]v1beta1.ClusterVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineDeploymentTopologyBuilder. +func (in *MachineDeploymentTopologyBuilder) DeepCopy() *MachineDeploymentTopologyBuilder { + if in == nil { + return nil + } + out := new(MachineDeploymentTopologyBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineHealthCheckBuilder) DeepCopyInto(out *MachineHealthCheckBuilder) { + *out = *in + if in.ownerRefs != nil { + in, out := &in.ownerRefs, &out.ownerRefs + *out = make([]v1.OwnerReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.selector.DeepCopyInto(&out.selector) + if in.conditions != nil { + in, out := &in.conditions, &out.conditions + *out = make([]v1beta1.UnhealthyCondition, len(*in)) + copy(*out, *in) + } + if in.maxUnhealthy != nil { + in, out := &in.maxUnhealthy, &out.maxUnhealthy + *out = new(intstr.IntOrString) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineHealthCheckBuilder. +func (in *MachineHealthCheckBuilder) DeepCopy() *MachineHealthCheckBuilder { + if in == nil { + return nil + } + out := new(MachineHealthCheckBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachinePoolBuilder) DeepCopyInto(out *MachinePoolBuilder) { + *out = *in + if in.bootstrap != nil { + in, out := &in.bootstrap, &out.bootstrap + *out = (*in).DeepCopy() + } + if in.infrastructure != nil { + in, out := &in.infrastructure, &out.infrastructure + *out = (*in).DeepCopy() + } + if in.version != nil { + in, out := &in.version, &out.version + *out = new(string) + **out = **in + } + if in.replicas != nil { + in, out := &in.replicas, &out.replicas + *out = new(int32) + **out = **in + } + if in.labels != nil { + in, out := &in.labels, &out.labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.status != nil { + in, out := &in.status, &out.status + *out = new(apiv1beta1.MachinePoolStatus) + (*in).DeepCopyInto(*out) + } + if in.minReadySeconds != nil { + in, out := &in.minReadySeconds, &out.minReadySeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachinePoolBuilder. +func (in *MachinePoolBuilder) DeepCopy() *MachinePoolBuilder { + if in == nil { + return nil + } + out := new(MachinePoolBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachinePoolClassBuilder) DeepCopyInto(out *MachinePoolClassBuilder) { + *out = *in + if in.infrastructureMachinePoolTemplate != nil { + in, out := &in.infrastructureMachinePoolTemplate, &out.infrastructureMachinePoolTemplate + *out = (*in).DeepCopy() + } + if in.bootstrapTemplate != nil { + in, out := &in.bootstrapTemplate, &out.bootstrapTemplate + *out = (*in).DeepCopy() + } + if in.labels != nil { + in, out := &in.labels, &out.labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.annotations != nil { + in, out := &in.annotations, &out.annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.failureDomains != nil { + in, out := &in.failureDomains, &out.failureDomains + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.nodeDrainTimeout != nil { + in, out := &in.nodeDrainTimeout, &out.nodeDrainTimeout + *out = new(v1.Duration) + **out = **in + } + if in.nodeVolumeDetachTimeout != nil { + in, out := &in.nodeVolumeDetachTimeout, &out.nodeVolumeDetachTimeout + *out = new(v1.Duration) + **out = **in + } + if in.nodeDeletionTimeout != nil { + in, out := &in.nodeDeletionTimeout, &out.nodeDeletionTimeout + *out = new(v1.Duration) + **out = **in + } + if in.minReadySeconds != nil { + in, out := &in.minReadySeconds, &out.minReadySeconds + *out = new(int32) + **out = **in + } + if in.namingStrategy != nil { + in, out := &in.namingStrategy, &out.namingStrategy + *out = new(v1beta1.MachinePoolClassNamingStrategy) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachinePoolClassBuilder. +func (in *MachinePoolClassBuilder) DeepCopy() *MachinePoolClassBuilder { + if in == nil { + return nil + } + out := new(MachinePoolClassBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachinePoolTopologyBuilder) DeepCopyInto(out *MachinePoolTopologyBuilder) { + *out = *in + if in.replicas != nil { + in, out := &in.replicas, &out.replicas + *out = new(int32) + **out = **in + } + if in.failureDomains != nil { + in, out := &in.failureDomains, &out.failureDomains + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.variables != nil { + in, out := &in.variables, &out.variables + *out = make([]v1beta1.ClusterVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachinePoolTopologyBuilder. +func (in *MachinePoolTopologyBuilder) DeepCopy() *MachinePoolTopologyBuilder { + if in == nil { + return nil + } + out := new(MachinePoolTopologyBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineSetBuilder) DeepCopyInto(out *MachineSetBuilder) { + *out = *in + if in.bootstrapTemplate != nil { + in, out := &in.bootstrapTemplate, &out.bootstrapTemplate + *out = (*in).DeepCopy() + } + if in.infrastructureTemplate != nil { + in, out := &in.infrastructureTemplate, &out.infrastructureTemplate + *out = (*in).DeepCopy() + } + if in.replicas != nil { + in, out := &in.replicas, &out.replicas + *out = new(int32) + **out = **in + } + if in.labels != nil { + in, out := &in.labels, &out.labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ownerRefs != nil { + in, out := &in.ownerRefs, &out.ownerRefs + *out = make([]v1.OwnerReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineSetBuilder. +func (in *MachineSetBuilder) DeepCopy() *MachineSetBuilder { + if in == nil { + return nil + } + out := new(MachineSetBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestBootstrapConfigBuilder) DeepCopyInto(out *TestBootstrapConfigBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestBootstrapConfigBuilder. +func (in *TestBootstrapConfigBuilder) DeepCopy() *TestBootstrapConfigBuilder { + if in == nil { + return nil + } + out := new(TestBootstrapConfigBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestBootstrapTemplateBuilder) DeepCopyInto(out *TestBootstrapTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestBootstrapTemplateBuilder. +func (in *TestBootstrapTemplateBuilder) DeepCopy() *TestBootstrapTemplateBuilder { + if in == nil { + return nil + } + out := new(TestBootstrapTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestControlPlaneBuilder) DeepCopyInto(out *TestControlPlaneBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestControlPlaneBuilder. +func (in *TestControlPlaneBuilder) DeepCopy() *TestControlPlaneBuilder { + if in == nil { + return nil + } + out := new(TestControlPlaneBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestControlPlaneTemplateBuilder) DeepCopyInto(out *TestControlPlaneTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestControlPlaneTemplateBuilder. +func (in *TestControlPlaneTemplateBuilder) DeepCopy() *TestControlPlaneTemplateBuilder { + if in == nil { + return nil + } + out := new(TestControlPlaneTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestInfrastructureClusterBuilder) DeepCopyInto(out *TestInfrastructureClusterBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestInfrastructureClusterBuilder. +func (in *TestInfrastructureClusterBuilder) DeepCopy() *TestInfrastructureClusterBuilder { + if in == nil { + return nil + } + out := new(TestInfrastructureClusterBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestInfrastructureClusterTemplateBuilder) DeepCopyInto(out *TestInfrastructureClusterTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestInfrastructureClusterTemplateBuilder. +func (in *TestInfrastructureClusterTemplateBuilder) DeepCopy() *TestInfrastructureClusterTemplateBuilder { + if in == nil { + return nil + } + out := new(TestInfrastructureClusterTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestInfrastructureMachinePoolBuilder) DeepCopyInto(out *TestInfrastructureMachinePoolBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestInfrastructureMachinePoolBuilder. +func (in *TestInfrastructureMachinePoolBuilder) DeepCopy() *TestInfrastructureMachinePoolBuilder { + if in == nil { + return nil + } + out := new(TestInfrastructureMachinePoolBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestInfrastructureMachinePoolTemplateBuilder) DeepCopyInto(out *TestInfrastructureMachinePoolTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestInfrastructureMachinePoolTemplateBuilder. +func (in *TestInfrastructureMachinePoolTemplateBuilder) DeepCopy() *TestInfrastructureMachinePoolTemplateBuilder { + if in == nil { + return nil + } + out := new(TestInfrastructureMachinePoolTemplateBuilder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestInfrastructureMachineTemplateBuilder) DeepCopyInto(out *TestInfrastructureMachineTemplateBuilder) { + *out = *in + if in.obj != nil { + in, out := &in.obj, &out.obj + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestInfrastructureMachineTemplateBuilder. +func (in *TestInfrastructureMachineTemplateBuilder) DeepCopy() *TestInfrastructureMachineTemplateBuilder { + if in == nil { + return nil + } + out := new(TestInfrastructureMachineTemplateBuilder) + in.DeepCopyInto(out) + return out +} diff --git a/internal/test/envtest/environment.go b/internal/test/envtest/environment.go new file mode 100644 index 000000000..a979a333d --- /dev/null +++ b/internal/test/envtest/environment.go @@ -0,0 +1,269 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package envtest + +import ( + "context" + "fmt" + "os" + "path" + "path/filepath" + goruntime "runtime" + "time" + + // TODO: https://github.com/kubernetes-sigs/cluster-api/releases/tag/v1.9.0-beta.0 publishes + // generic providers used for testing. Use package from upstream project when bump to + // cluster-api@v1.9.0 instead copy-paste package. + // More info here: https://github.com/kubernetes-sigs/cluster-api/pull/11356 + "github.com/k0sproject/k0smotron/internal/test/builder" + "github.com/onsi/ginkgo/v2" + "github.com/pkg/errors" + "golang.org/x/tools/go/packages" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kerrors "k8s.io/apimachinery/pkg/util/errors" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/klog/v2" + "sigs.k8s.io/cluster-api/cmd/clusterctl/log" + "sigs.k8s.io/cluster-api/util/kubeconfig" + + bootstrapv1beta1 "github.com/k0sproject/k0smotron/api/bootstrap/v1beta1" + controlplanev1beta1 "github.com/k0sproject/k0smotron/api/controlplane/v1beta1" + infrastructurev1beta1 "github.com/k0sproject/k0smotron/api/infrastructure/v1beta1" + k0smotronv1beta1 "github.com/k0sproject/k0smotron/api/k0smotron.io/v1beta1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +var ( + cacheSyncBackoff = wait.Backoff{ + Duration: 100 * time.Millisecond, + Factor: 1.5, + Steps: 8, + Jitter: 0.4, + } +) + +// Environment encapsulates a Kubernetes local test environment. +type Environment struct { + manager.Manager + client.Client + Config *rest.Config + env *envtest.Environment + cancel context.CancelFunc +} + +func init() { + logger := klog.Background() + // Use klog as the internal logger for this envtest environment. + log.SetLogger(logger) + // Additionally force all controllers to use the Ginkgo logger. + ctrl.SetLogger(logger) + // Add logger for ginkgo. + klog.SetOutput(ginkgo.GinkgoWriter) + + utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(clusterv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(k0smotronv1beta1.AddToScheme(scheme.Scheme)) + utilruntime.Must(bootstrapv1beta1.AddToScheme(scheme.Scheme)) + utilruntime.Must(controlplanev1beta1.AddToScheme(scheme.Scheme)) + utilruntime.Must(infrastructurev1beta1.AddToScheme(scheme.Scheme)) +} + +func newEnvironment() *Environment { + _, filename, _, _ := goruntime.Caller(0) + root := path.Join(path.Dir(filename), "..", "..", "..") + + capiCoreCrdsPath := "" + if capiCoreCrdsPath = getFilePathToCAPICoreCRDs(); capiCoreCrdsPath == "" { + panic(fmt.Errorf("failed to retrieve cluster-api core crds path")) + } + + crdPaths := []string{ + capiCoreCrdsPath, + filepath.Join(root, "config", "clusterapi", "bootstrap", "bases"), + filepath.Join(root, "config", "clusterapi", "controlplane", "bases"), + filepath.Join(root, "config", "clusterapi", "infrastructure", "bases"), + filepath.Join(root, "config", "clusterapi", "k0smotron.io", "bases"), + } + + env := &envtest.Environment{ + ErrorIfCRDPathMissing: true, + CRDDirectoryPaths: crdPaths, + CRDs: []*apiextensionsv1.CustomResourceDefinition{ + builder.GenericInfrastructureMachineCRD.DeepCopy(), + builder.GenericInfrastructureMachineTemplateCRD.DeepCopy(), + }, + } + + if _, err := env.Start(); err != nil { + panic(err) + } + + options := manager.Options{ + Scheme: scheme.Scheme, + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + } + + mgr, err := ctrl.NewManager(env.Config, options) + if err != nil { + panic(fmt.Errorf("failed to start testenv manager: %w", err)) + } + + if kubeconfigPath := os.Getenv("CAPI_TEST_ENV_KUBECONFIG"); kubeconfigPath != "" { + klog.Infof("Writing test env kubeconfig to %q", kubeconfigPath) + config := kubeconfig.FromEnvTestConfig(env.Config, &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + }) + if err := os.WriteFile(kubeconfigPath, config, 0o600); err != nil { + panic(errors.Wrapf(err, "failed to write the test env kubeconfig")) + } + } + + return &Environment{ + Manager: mgr, + Client: mgr.GetClient(), + Config: mgr.GetConfig(), + env: env, + } +} + +func Build(ctx context.Context) *Environment { + testEnv := newEnvironment() + go func() { + fmt.Println("Starting the manager") + if err := testEnv.StartManager(ctx); err != nil { + panic(fmt.Sprintf("Failed to start the envtest manager: %v", err)) + } + }() + + return testEnv +} + +func (e *Environment) Teardown() { + e.cancel() + if err := e.Stop(); err != nil { + panic(fmt.Sprintf("Failed to stop envtest: %v", err)) + } +} + +// StartManager starts the test controller against the local API server. +func (e *Environment) StartManager(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + e.cancel = cancel + return e.Manager.Start(ctx) +} + +// Stop stops the test environment. +func (e *Environment) Stop() error { + e.cancel() + return e.env.Stop() +} + +func getFilePathToCAPICoreCRDs() string { + packageName := "sigs.k8s.io/cluster-api" + packageConfig := &packages.Config{ + Mode: packages.NeedModule, + } + + pkgs, err := packages.Load(packageConfig, packageName) + if err != nil { + return "" + } + + return filepath.Join(pkgs[0].Module.Dir, "config", "crd", "bases") +} + +// CreateNamespace creates a new namespace with a generated name. +func (e *Environment) CreateNamespace(ctx context.Context, generateName string) (*v1.Namespace, error) { + ns := &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-", generateName), + Labels: map[string]string{ + "testenv/original-name": generateName, + }, + }, + } + if err := e.Client.Create(ctx, ns); err != nil { + return nil, err + } + + return ns, nil +} + +// Cleanup removes objects from the Environment. +func (e *Environment) Cleanup(ctx context.Context, objs ...client.Object) error { + errs := make([]error, 0, len(objs)) + for _, o := range objs { + err := e.Client.Delete(ctx, o) + if apierrors.IsNotFound(err) { + // If the object is not found, it must've been garbage collected + // already. For example, if we delete namespace first and then + // objects within it. + continue + } + errs = append(errs, err) + } + + return kerrors.NewAggregate(errs) +} + +// CleanupAndWait deletes all the given objects and waits for the cache to be updated accordingly. +// +// NOTE: Waiting for the cache to be updated helps in preventing test flakes due to the cache sync delays. +func (e *Environment) CleanupAndWait(ctx context.Context, objs ...client.Object) error { + if err := e.Cleanup(ctx, objs...); err != nil { + return err + } + + // Makes sure the cache is updated with the deleted object + errs := []error{} + for _, o := range objs { + // Ignoring namespaces because in testenv the namespace cleaner is not running. + if o.GetObjectKind().GroupVersionKind().GroupKind() == v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() { + continue + } + + oCopy := o.DeepCopyObject().(client.Object) + key := client.ObjectKeyFromObject(o) + err := wait.ExponentialBackoff( + cacheSyncBackoff, + func() (done bool, err error) { + if err := e.Get(ctx, key, oCopy); err != nil { + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + } + return false, nil + }) + errs = append(errs, errors.Wrapf(err, "key %s, %s is not being deleted from the testenv client cache", o.GetObjectKind().GroupVersionKind().String(), key)) + } + return kerrors.NewAggregate(errs) +}