diff --git a/controllers/controllers_test.go b/controllers/controllers_test.go new file mode 100644 index 00000000..7293922e --- /dev/null +++ b/controllers/controllers_test.go @@ -0,0 +1,380 @@ +/* +(c) Copyright IBM Corp. 2024 +(c) Copyright Instana Inc. + +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 controllers + +import ( + "context" + "path/filepath" + "testing" + "time" + + instanav1 "github.com/instana/instana-agent-operator/api/v1" + "github.com/instana/instana-agent-operator/pkg/collections/list" + instanaclient "github.com/instana/instana-agent-operator/pkg/k8s/client" + "github.com/instana/instana-agent-operator/pkg/k8s/object/builders/common/helpers" + "github.com/instana/instana-agent-operator/pkg/pointer" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +var agentNamespace = types.NamespacedName{ + Name: "instana-agent", + Namespace: "default", +} + +// The agent schema that will be used throughout the tests +var agent = &instanav1.InstanaAgent{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "instana.io/v1", + Kind: "InstanaAgent", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: agentNamespace.Name, + Namespace: agentNamespace.Namespace, + Finalizers: []string{"test"}, + }, + Spec: instanav1.InstanaAgentSpec{ + Zone: instanav1.Name{Name: "test"}, + Cluster: instanav1.Name{Name: "test"}, + Agent: instanav1.BaseAgentSpec{ + Key: "test", + EndpointHost: "ingress-red-saas.instana.io", + EndpointPort: "443", + }, + K8sSensor: instanav1.K8sSpec{ + PodDisruptionBudget: instanav1.Enabled{Enabled: pointer.To(true)}, + }, + }, +} + +type object struct { + gvk schema.GroupVersionKind + key types.NamespacedName +} + +// number of agent resources used for diffing whether the controller functions properly +var ( + agentConfigMap = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "ConfigMap", + }, + key: agentNamespace, + } + agentDaemonset = object{ + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "DaemonSet", + }, + key: agentNamespace, + } + agentHeadlessService = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "Service", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-headless", + Namespace: agentNamespace.Namespace, + }, + } + agentService = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "Service", + }, + key: agentNamespace, + } + agentKeysSecret = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "Secret", + }, + key: agentNamespace, + } + agentContainerSecret = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "Secret", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-containers-instana-io", + Namespace: agentNamespace.Namespace, + }, + } + agentServiceAccount = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "ServiceAccount", + }, + key: agentNamespace, + } +) + +// number of k8sensor resources used for diffing whether the controller functions properly +var ( + k8SensorConfigMap = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "ConfigMap", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-k8sensor", + Namespace: agentNamespace.Namespace, + }, + } + k8SensorDeployment = object{ + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-k8sensor", + Namespace: agentNamespace.Namespace, + }, + } + k8SensorPdb = object{ + gvk: schema.GroupVersionKind{ + Group: "policy", + Version: "v1", + Kind: "PodDisruptionBudget", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-k8sensor", + Namespace: agentNamespace.Namespace, + }, + } + k8SensorClusterRole = object{ + gvk: schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-k8sensor", + Namespace: agentNamespace.Namespace, + }, + } + k8SensorClusterRoleBinding = object{ + gvk: schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRoleBinding", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-k8sensor", + Namespace: agentNamespace.Namespace, + }, + } + k8SensorServiceAccount = object{ + gvk: schema.GroupVersionKind{ + Version: "v1", + Kind: "ServiceAccount", + }, + key: client.ObjectKey{ + Name: agentNamespace.Name + "-k8sensor", + Namespace: agentNamespace.Namespace, + }, + } +) + +// TestInstanaAgentControllerTestSuite is the method that is called to run InstanaAgentControllerTestSuite +func TestInstanaAgentControllerTestSuite(t *testing.T) { + suite.Run(t, new(InstanaAgentControllerTestSuite)) +} + +type InstanaAgentControllerTestSuite struct { + suite.Suite + testEnv *envtest.Environment + k8sClient client.Client + instanaAgentClient instanaclient.InstanaAgentClient + scheme *runtime.Scheme + ctx context.Context + cancel context.CancelFunc +} + +// SetupSuite prepares the controller package for testing i.e. BeforeSuite +func (suite *InstanaAgentControllerTestSuite) SetupSuite() { + suite.ctx, suite.cancel = context.WithCancel(context.Background()) + + // Prepare scheme with instana scheme + suite.scheme = runtime.NewScheme() + err := scheme.AddToScheme(suite.scheme) + require.NoError(suite.T(), err) + + // Add instana agent types to scheme + err = instanav1.AddToScheme(suite.scheme) + require.NoError(suite.T(), err) + + // Prepare environment + suite.testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + CRDInstallOptions: envtest.CRDInstallOptions{CleanUpAfterUse: true}, + Scheme: scheme.Scheme, + } + + cfg, err := suite.testEnv.Start() + require.NoError(suite.T(), err) + require.NotNil(suite.T(), cfg) + + // Prepare clients and most importantly Instana Agent Client + suite.k8sClient, err = client.New(cfg, client.Options{Scheme: suite.scheme}) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), suite.k8sClient) + suite.instanaAgentClient = instanaclient.NewInstanaAgentClient(suite.k8sClient) + + // Start the manager and controller + mgr, err := ctrl.NewManager(cfg, ctrl.Options{Scheme: suite.scheme}) + require.NoError(suite.T(), err) + err = Add(mgr) + require.NoError(suite.T(), err) + + go func() { + err = mgr.Start(suite.ctx) + require.NoError(suite.T(), err) + }() +} + +// TearDownSuite i.e. AfterSuite +func (suite *InstanaAgentControllerTestSuite) TearDownSuite() { + suite.cancel() + err := suite.testEnv.Stop() + require.NoError(suite.T(), err) +} + +// all is a utility method to iterate through objects and use the user defined validation function to verify validity +func (suite *InstanaAgentControllerTestSuite) all(validatorFunc func(object) bool, o ...object) func() bool { + return func() bool { + return list.NewConditions(o).All(validatorFunc) + } +} + +// exist is a utility method to unwrap the result struct of InstanaAgentClient and return whether the obj existed +func (suite *InstanaAgentControllerTestSuite) exist(obj object) bool { + exists, _ := suite.instanaAgentClient.Exists(suite.ctx, obj.gvk, obj.key).Get() + return exists +} + +// notExist is a utility method to unwrap the result struct of InstanaAgentClient and return whether the obj didn't exist +func (suite *InstanaAgentControllerTestSuite) notExist(obj object) bool { + exists, _ := suite.instanaAgentClient.Exists(suite.ctx, obj.gvk, obj.key).Get() + return !exists +} + +// TestInstanaAgentCR is the test method to verify the whole lifecycle of the Instana Agent custom resource from start to deletion against the EnvTest +func (suite *InstanaAgentControllerTestSuite) TestInstanaAgentCR() { + _, err := suite.instanaAgentClient.Apply(suite.ctx, agent).Get() + require.NoError(suite.T(), err, "Should not throw an error when applying the InstanaAgent schema") + + require.Eventually(suite.T(), + suite.all( + suite.exist, + agentConfigMap, + agentDaemonset, + agentHeadlessService, + agentService, + agentServiceAccount, + agentKeysSecret, + k8SensorConfigMap, + k8SensorDeployment, + k8SensorServiceAccount, + k8SensorClusterRole, + k8SensorClusterRoleBinding, + k8SensorPdb, + ), + 10*time.Second, + time.Second, + "Should contain all objects in the schema", + ) + + agentNew := agent.DeepCopy() + agentNew.Spec.K8sSensor.PodDisruptionBudget.Enabled = pointer.To(false) + agentNew.Spec.Agent.KeysSecret = "test" + agentNew.Spec.Agent.ImageSpec.Name = helpers.ContainersInstanaIORegistry + "/instana-agent" + err = suite.instanaAgentClient.Patch( + suite.ctx, + agentNew, + client.MergeFrom(agent), + ) + require.NoError(suite.T(), err, "Should not throw an error when patching the InstanaAgent schema with a new version") + + require.Eventually(suite.T(), + suite.all( + suite.exist, + agentConfigMap, + agentDaemonset, + agentHeadlessService, + agentService, + agentServiceAccount, + agentContainerSecret, + k8SensorConfigMap, + k8SensorDeployment, + k8SensorServiceAccount, + k8SensorClusterRole, + k8SensorClusterRoleBinding, + ), + 10*time.Second, + time.Second, + "Should contain listed objects in the patched schema", + ) + require.Eventually(suite.T(), + suite.all( + suite.notExist, + agentKeysSecret, + k8SensorPdb, + ), + 10*time.Second, + time.Second, + "Should not contain listed objects after the patched schema", + ) + + err = suite.k8sClient.Delete(suite.ctx, agent) + require.NoError(suite.T(), err, "Should not return an error while deleting the agent") + require.Eventually(suite.T(), + suite.all( + suite.notExist, + agentConfigMap, + agentDaemonset, + agentHeadlessService, + agentService, + agentServiceAccount, + agentKeysSecret, + agentContainerSecret, + k8SensorConfigMap, + k8SensorDeployment, + k8SensorServiceAccount, + k8SensorClusterRole, + k8SensorClusterRoleBinding, + k8SensorPdb, + ), + 10*time.Second, + time.Second, + "Should delete all objects from the schema", + ) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go deleted file mode 100644 index e7884017..00000000 --- a/controllers/suite_test.go +++ /dev/null @@ -1,443 +0,0 @@ -/* -(c) Copyright IBM Corp. 2024 -(c) Copyright Instana Inc. - -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 controllers - -import ( - "context" - "path/filepath" - "testing" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - instanav1 "github.com/instana/instana-agent-operator/api/v1" - "github.com/instana/instana-agent-operator/pkg/collections/list" - instanaclient "github.com/instana/instana-agent-operator/pkg/k8s/client" - "github.com/instana/instana-agent-operator/pkg/k8s/object/builders/common/helpers" - "github.com/instana/instana-agent-operator/pkg/pointer" - // +kubebuilder:scaffold:imports -) - -const ( - instanaAgentName = "instana-agent" - instanaAgentNamespace = "default" -) - -var agent = &instanav1.InstanaAgent{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "instana.io/v1", - Kind: "InstanaAgent", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: instanaAgentName, - Namespace: instanaAgentNamespace, - Finalizers: []string{"test"}, - }, - Spec: instanav1.InstanaAgentSpec{ - Zone: instanav1.Name{Name: "test"}, - Cluster: instanav1.Name{Name: "test"}, - Agent: instanav1.BaseAgentSpec{ - Key: "test", - EndpointHost: "ingress-red-saas.instana.io", - EndpointPort: "443", - }, - K8sSensor: instanav1.K8sSpec{ - PodDisruptionBudget: instanav1.Enabled{Enabled: pointer.To(true)}, - }, - }, -} - -type object struct { - gvk schema.GroupVersionKind - key types.NamespacedName -} - -var instanaAgentObjectKey types.NamespacedName = types.NamespacedName{ - Name: instanaAgentName, - Namespace: instanaAgentNamespace, -} - -// agent resources -var ( - agentConfigMap = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "ConfigMap", - }, - key: instanaAgentObjectKey, - } - agentDaemonset = object{ - gvk: schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "DaemonSet", - }, - key: instanaAgentObjectKey, - } - agentHeadlessService = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "Service", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-headless", - Namespace: instanaAgentNamespace, - }, - } - agentService = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "Service", - }, - key: instanaAgentObjectKey, - } - agentKeysSecret = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "Secret", - }, - key: instanaAgentObjectKey, - } - agentContainerSecret = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "Secret", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-containers-instana-io", - Namespace: instanaAgentNamespace, - }, - } - agentServiceAccount = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "ServiceAccount", - }, - key: instanaAgentObjectKey, - } -) - -// k8sensor resources -var ( - k8SensorConfigMap = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "ConfigMap", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-k8sensor", - Namespace: instanaAgentNamespace, - }, - } - k8SensorDeployment = object{ - gvk: schema.GroupVersionKind{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-k8sensor", - Namespace: instanaAgentNamespace, - }, - } - k8SensorPdb = object{ - gvk: schema.GroupVersionKind{ - Group: "policy", - Version: "v1", - Kind: "PodDisruptionBudget", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-k8sensor", - Namespace: instanaAgentNamespace, - }, - } - k8SensorClusterRole = object{ - gvk: schema.GroupVersionKind{ - Group: "rbac.authorization.k8s.io", - Version: "v1", - Kind: "ClusterRole", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-k8sensor", - Namespace: instanaAgentNamespace, - }, - } - k8SensorClusterRoleBinding = object{ - gvk: schema.GroupVersionKind{ - Group: "rbac.authorization.k8s.io", - Version: "v1", - Kind: "ClusterRoleBinding", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-k8sensor", - Namespace: instanaAgentNamespace, - }, - } - k8SensorServiceAccount = object{ - gvk: schema.GroupVersionKind{ - Version: "v1", - Kind: "ServiceAccount", - }, - key: client.ObjectKey{ - Name: instanaAgentName + "-k8sensor", - Namespace: instanaAgentNamespace, - }, - } -) - -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -var cfg *rest.Config -var k8sClient client.Client -var instanaAgentClient instanaclient.InstanaAgentClient -var testEnv *envtest.Environment -var mgrCancel context.CancelFunc -var ctx context.Context -var cancel context.CancelFunc - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecs( - t, - "Controller Suite", - ) -} - -var _ = BeforeSuite( - func() { - ctx, cancel = context.WithCancel(context.Background()) - - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - err := instanav1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - CRDInstallOptions: envtest.CRDInstallOptions{CleanUpAfterUse: true}, - Scheme: scheme.Scheme, - } - - cfg, err = testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - // +kubebuilder:scaffold:scheme - - // Set up the Manager as we'd do in the main.go, but disable some (unneeded) config and use the above cluster configuration - k8sManager, err := ctrl.NewManager( - cfg, ctrl.Options{ - Scheme: scheme.Scheme, - }, - ) - Expect(err).ToNot(HaveOccurred()) - - k8sClient = k8sManager.GetClient() - instanaAgentClient = instanaclient.NewInstanaAgentClient(k8sClient) - - // Create the Reconciler / Controller and register with the Manager (just like in the main.go) - err = Add(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - var mgrCtx context.Context - mgrCtx, mgrCancel = context.WithCancel(ctrl.SetupSignalHandler()) - - go func() { - err = k8sManager.Start(mgrCtx) - Expect(err).ToNot(HaveOccurred()) - }() - - }, 60, -) - -var _ = AfterSuite( - func() { - By("tearing down the test environment") - if mgrCancel != nil { - mgrCancel() - } - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) - - cancel() - }, -) - -func exist(obj object) bool { - res, err := instanaAgentClient.Exists(ctx, obj.gvk, obj.key).Get() - return res && err == nil -} - -func allExist(o ...object) func() bool { - return func() bool { - objects := list.NewConditions(o) - - return objects.All(exist) - } -} - -func doNotExist(obj object) bool { - res, err := instanaAgentClient.Exists(ctx, obj.gvk, obj.key).Get() - return !res && err == nil -} - -func noneExist(o ...object) func() bool { - return func() bool { - objects := list.NewConditions(o) - - return objects.All(doNotExist) - } -} - -func failTest(err error) { - Expect(err).NotTo(HaveOccurred()) -} - -var _ = Describe( - "An InstanaAgent CR", func() { - When( - "the CR is created", func() { - Specify( - "using the k8s client", func() { - instanaAgentClient.Apply(ctx, agent).OnFailure(failTest) - }, - ) - Specify( - "the controller should create all of the expected resources", func() { - Eventually( - allExist( - agentConfigMap, - agentDaemonset, - agentHeadlessService, - agentService, - agentServiceAccount, - agentKeysSecret, - k8SensorConfigMap, - k8SensorDeployment, - k8SensorServiceAccount, - k8SensorClusterRole, - k8SensorClusterRoleBinding, - k8SensorPdb, - ), - ). - Within(10 * time.Second). - ProbeEvery(time.Second). - Should(BeTrue()) - }, - ) - }, - ) - When( - "the CR is updated", func() { - Specify( - "using the k8s client", func() { - agentNew := agent.DeepCopy() - - agentNew.Spec.K8sSensor.PodDisruptionBudget.Enabled = pointer.To(false) - agentNew.Spec.Agent.KeysSecret = "test" - agentNew.Spec.Agent.ImageSpec.Name = helpers.ContainersInstanaIORegistry + "/instana-agent" - - err := k8sClient.Patch(ctx, agentNew, client.MergeFrom(agent)) - Expect(err).NotTo(HaveOccurred()) - }, - ) - Specify( - "the controller should update the resources", func() { - Eventually( - allExist( - agentConfigMap, - agentDaemonset, - agentHeadlessService, - agentService, - agentServiceAccount, - agentContainerSecret, - k8SensorConfigMap, - k8SensorDeployment, - k8SensorServiceAccount, - k8SensorClusterRole, - k8SensorClusterRoleBinding, - ), - ). - Within(10 * time.Second). - ProbeEvery(time.Second). - Should(BeTrue()) - Eventually( - noneExist( - agentKeysSecret, - k8SensorPdb, - ), - ). - Within(10 * time.Second). - ProbeEvery(time.Second). - Should(BeTrue()) - }, - ) - }, - ) - When( - "the CR is deleted", func() { - Specify( - "using the k8s client", func() { - err := k8sClient.Delete(ctx, agent) - Expect(err).NotTo(HaveOccurred()) - }, - ) - Specify( - "the controller should delete all of the expected resources", func() { - Eventually( - noneExist( - agentConfigMap, - agentDaemonset, - agentHeadlessService, - agentService, - agentServiceAccount, - agentKeysSecret, - agentContainerSecret, - k8SensorConfigMap, - k8SensorDeployment, - k8SensorServiceAccount, - k8SensorClusterRole, - k8SensorClusterRoleBinding, - k8SensorPdb, - ), - ). - Within(10 * time.Second). - ProbeEvery(time.Second). - Should(BeTrue()) - }, - ) - }, - ) - }, -) diff --git a/go.mod b/go.mod index 9cf78697..a390f32e 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,6 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/go-errors/errors v1.4.2 github.com/go-logr/logr v1.4.1 - github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.31.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 go.uber.org/mock v0.4.0