diff --git a/go.mod b/go.mod index b48da3961..37658506d 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( ) require ( + github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 4d418cc65..f2974d197 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= diff --git a/internal/test/helpers/webhook.go b/internal/test/helpers/webhook.go new file mode 100644 index 000000000..7b198e865 --- /dev/null +++ b/internal/test/helpers/webhook.go @@ -0,0 +1,125 @@ +/* +Copyright 2023 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 helpers + +import ( + "fmt" + "net" + "os" + "path" + "path/filepath" + goruntime "runtime" + "strconv" + "time" + + v1 "k8s.io/api/admissionregistration/v1" + "k8s.io/klog/v2" + utilyaml "sigs.k8s.io/cluster-api/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +const ( + mutatingWebhookKind = "MutatingWebhookConfiguration" + validatingWebhookKind = "ValidatingWebhookConfiguration" + mutatingwebhook = "mutating-webhook-configuration" + validatingwebhook = "validating-webhook-configuration" +) + +// Mutate the name of each webhook, because kubebuilder generates the same name for all controllers. +// In normal usage, kustomize will prefix the controller name, which we have to do manually here. +func appendWebhookConfiguration(configyamlFile []byte, tag string) ([]*v1.MutatingWebhookConfiguration, []*v1.ValidatingWebhookConfiguration, error) { + var mutatingWebhooks []*v1.MutatingWebhookConfiguration + var validatingWebhooks []*v1.ValidatingWebhookConfiguration + objs, err := utilyaml.ToUnstructured(configyamlFile) + if err != nil { + klog.Fatalf("failed to parse yaml") + } + // look for resources of kind MutatingWebhookConfiguration + for i := range objs { + o := objs[i] + if o.GetKind() == mutatingWebhookKind { + // update the name in metadata + if o.GetName() == mutatingwebhook { + var m v1.MutatingWebhookConfiguration + o.SetName(mutatingwebhook + "-" + tag) + if err := scheme.Convert(&o, &m, nil); err != nil { + return nil, nil, fmt.Errorf("failed to convert scheme: %w", err) + } + mutatingWebhooks = append(mutatingWebhooks, &m) + } + } + if o.GetKind() == validatingWebhookKind { + // update the name in metadata + if o.GetName() == validatingwebhook { + var v v1.ValidatingWebhookConfiguration + o.SetName(validatingwebhook + "-" + tag) + if err := scheme.Convert(&o, &v, nil); err != nil { + return nil, nil, fmt.Errorf("failed to convert scheme: %w", err) + } + validatingWebhooks = append(validatingWebhooks, &v) + } + } + } + return mutatingWebhooks, validatingWebhooks, nil +} + +func initializeWebhookInEnvironment() { + // Get the root of the current file to use in CRD paths. + _, filename, _, _ := goruntime.Caller(0) + root := path.Join(path.Dir(filename), "..", "..", "..") + corepath := filepath.Join(root, "config", "webhook", "manifests.yaml") + configyamlFile, err := os.ReadFile(corepath) //#nosec + if err != nil { + klog.Fatalf("Failed to read core webhook configuration file: %v", err) + } + if err != nil { + klog.Fatalf("failed to parse yaml") + } + // append the webhook with suffix to avoid clashing webhooks. repeated for every webhook + mutatingWebhooks, validatingWebhooks, err := appendWebhookConfiguration(configyamlFile, "config") + if err != nil { + klog.Fatalf("Failed to append core controller webhook config: %v", err) + } + + env.WebhookInstallOptions = envtest.WebhookInstallOptions{ + MaxTime: 20 * time.Second, + PollInterval: time.Second, + ValidatingWebhooks: validatingWebhooks, + MutatingWebhooks: mutatingWebhooks, + } +} + +// WaitForWebhooks waits for webhook port to be ready. +func (*TestEnvironment) WaitForWebhooks() { + port := env.WebhookInstallOptions.LocalServingPort + + klog.V(2).Infof("Waiting for webhook port %d to be open prior to running tests", port) + timeout := 1 * time.Second + for { + time.Sleep(1 * time.Second) + conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), timeout) + if err != nil { + klog.V(2).Infof("Webhook port is not ready, will retry in %v: %s", timeout, err) + continue + } + if err := conn.Close(); err != nil { + klog.Fatalf("failed to close connection: %s", err) + } + klog.V(2).Info("Webhook port is now open. Continuing with tests...") + return + } +} diff --git a/internal/test/integration/github/integration_suite_test.go b/internal/test/integration/github/integration_suite_test.go new file mode 100644 index 000000000..d7221877d --- /dev/null +++ b/internal/test/integration/github/integration_suite_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2023 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 github + +import ( + "testing" + "time" + + "github.com/SovereignCloudStack/cluster-stack-operator/internal/controller" + "github.com/SovereignCloudStack/cluster-stack-operator/internal/test/helpers" + githubclient "github.com/SovereignCloudStack/cluster-stack-operator/pkg/github/client" + "github.com/SovereignCloudStack/cluster-stack-operator/pkg/kube" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ctrl "sigs.k8s.io/controller-runtime" + controllerruntimecontroller "sigs.k8s.io/controller-runtime/pkg/controller" +) + +const ( + timeout = time.Second * 10 + interval = 1000 * time.Millisecond +) + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} + +var ( + ctx = ctrl.SetupSignalHandler() + testEnv *helpers.TestEnvironment +) + +var _ = BeforeSuite(func() { + testEnv = helpers.NewTestEnvironment() + Expect((&controller.ClusterStackReconciler{ + Client: testEnv.Manager.GetClient(), + GitHubClientFactory: githubclient.NewFactory(), + ReleaseDirectory: "/tmp/downloads", + }).SetupWithManager(ctx, testEnv.Manager, controllerruntimecontroller.Options{})).To(Succeed()) + Expect((&controller.ClusterStackReleaseReconciler{ + Client: testEnv.Manager.GetClient(), + RESTConfig: testEnv.Manager.GetConfig(), + KubeClientFactory: kube.NewFactory(), + GitHubClientFactory: githubclient.NewFactory(), + ReleaseDirectory: "/tmp/downloads", + }).SetupWithManager(ctx, testEnv.Manager, controllerruntimecontroller.Options{})).To(Succeed()) + + go func() { + defer GinkgoRecover() + Expect(testEnv.StartManager(ctx)).To(Succeed()) + }() + <-testEnv.Manager.Elected() + // wait for webhook port to be open prior to running tests + testEnv.WaitForWebhooks() +}) + +var _ = AfterSuite(func() { + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/internal/test/integration/github/integration_test.go b/internal/test/integration/github/integration_test.go new file mode 100644 index 000000000..0fb7d7002 --- /dev/null +++ b/internal/test/integration/github/integration_test.go @@ -0,0 +1,186 @@ +/* +Copyright 2023 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 github + +import ( + csov1alpha1 "github.com/SovereignCloudStack/cluster-stack-operator/api/v1alpha1" + "github.com/SovereignCloudStack/cluster-stack-operator/pkg/clusterstack" + "github.com/SovereignCloudStack/cluster-stack-operator/pkg/test/utils" + csv "github.com/SovereignCloudStack/cluster-stack-operator/pkg/version" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + provider = "docker" + name = "ferrol" + kubernetesVersion = "1.27" + version = "v1" +) + +var _ = Describe("ClusterStackReconciler", func() { + Context("autosubscribe false", func() { + var ( + clusterStack *csov1alpha1.ClusterStack + testNs *corev1.Namespace + clusterStackReleaseKey types.NamespacedName + ) + + BeforeEach(func() { + var err error + testNs, err = testEnv.CreateNamespace(ctx, "clusterstack-integration") + Expect(err).NotTo(HaveOccurred()) + + clusterStack = &csov1alpha1.ClusterStack{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Namespace: testNs.Name, + }, + Spec: csov1alpha1.ClusterStackSpec{ + Provider: provider, + Name: name, + KubernetesVersion: kubernetesVersion, + Versions: []string{version}, + AutoSubscribe: false, + NoProvider: true, + }, + } + Expect(testEnv.Create(ctx, clusterStack)).To(Succeed()) + + cs, err := clusterstack.New(clusterStack.Spec.Provider, clusterStack.Spec.Name, clusterStack.Spec.KubernetesVersion, version) + Expect(err).To(BeNil()) + + clusterStackReleaseKey = types.NamespacedName{Name: cs.String(), Namespace: testNs.Name} + }) + + AfterEach(func() { + Expect(testEnv.Cleanup(ctx, testNs, clusterStack)).To(Succeed()) + }) + + It("creates the cluster stack release object", func() { + Eventually(func() error { + var clusterStackRelease csov1alpha1.ClusterStackRelease + return testEnv.Get(ctx, clusterStackReleaseKey, &clusterStackRelease) + }, timeout, interval).Should(BeNil()) + }) + + It("sets ClusterStackReleaseDownloaded condition once ClusterStackRelease object is created", func() { + Eventually(func() bool { + var clusterStackRelease csov1alpha1.ClusterStackRelease + return utils.IsPresentAndTrue(ctx, testEnv.Client, clusterStackReleaseKey, &clusterStackRelease, csov1alpha1.ClusterStackReleaseAssetsReadyCondition) + }, timeout, interval).Should(BeTrue()) + }) + + It("sets ClusterStackRelease Status ready after ClusterStackRelease object is created", func() { + Eventually(func() bool { + var foundClusterStackRelease csov1alpha1.ClusterStackRelease + if err := testEnv.Get(ctx, clusterStackReleaseKey, &foundClusterStackRelease); err != nil { + return foundClusterStackRelease.Status.Ready + } + return false + }, timeout, interval).Should(BeTrue()) + }) + }) + + Context("autosubscribe true", func() { + var ( + clusterStack *csov1alpha1.ClusterStack + testNs *corev1.Namespace + clusterStackKey types.NamespacedName + clusterStackReleaseKey types.NamespacedName + ) + + BeforeEach(func() { + var err error + testNs, err = testEnv.CreateNamespace(ctx, "clusterstack-integration") + Expect(err).NotTo(HaveOccurred()) + + clusterStack = &csov1alpha1.ClusterStack{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test2", + Namespace: testNs.Name, + }, + Spec: csov1alpha1.ClusterStackSpec{ + Provider: provider, + Name: name, + KubernetesVersion: kubernetesVersion, + Versions: []string{}, + AutoSubscribe: true, + NoProvider: true, + ProviderRef: nil, + }, + } + Expect(testEnv.Create(ctx, clusterStack)).To(Succeed()) + + clusterStackKey = types.NamespacedName{Name: clusterStack.Name, Namespace: testNs.Name} + + cs, err := clusterstack.New(clusterStack.Spec.Provider, clusterStack.Spec.Name, clusterStack.Spec.KubernetesVersion, version) + Expect(err).To(BeNil()) + + clusterStackReleaseKey = types.NamespacedName{Name: cs.String(), Namespace: testNs.Name} + }) + + AfterEach(func() { + Expect(testEnv.Cleanup(ctx, testNs, clusterStack)).To(Succeed()) + }) + + It("finds the new cluster stack release", func() { + Eventually(func() bool { + var clusterStackReleaseList csov1alpha1.ClusterStackReleaseList + if err := testEnv.List(ctx, &clusterStackReleaseList, &client.ListOptions{Namespace: testNs.Name}); err != nil { + testEnv.GetLogger().Error(err, "failed to get clusterStackList") + return false + } + + var clusterStack csov1alpha1.ClusterStack + if err := testEnv.Get(ctx, clusterStackKey, &clusterStack); err != nil { + testEnv.GetLogger().Error(err, "failed to get clusterStack", "key", clusterStackKey) + return false + } + + for _, csrSummary := range clusterStack.Status.Summary { + v, err := csv.New(csrSummary.Name) + Expect(err).To(BeNil()) + + oldVersion, err := csv.New(version) + Expect(err).To(BeNil()) + + cmp, err := v.Compare(oldVersion) + Expect(err).To(BeNil()) + + if cmp >= 0 { + return true + } + } + return false + }, timeout, interval).Should(BeTrue()) + }) + + It("creates the new cluster stack release", func() { + Eventually(func() bool { + var clusterStackRelease csov1alpha1.ClusterStackRelease + if err := testEnv.Get(ctx, clusterStackReleaseKey, &clusterStackRelease); err != nil { + testEnv.GetLogger().Error(err, "failed to get clusterStackRelease", "key", clusterStackReleaseKey) + return false + } + + return true + }, timeout, interval).Should(BeTrue()) + }) + }) +})