diff --git a/.gitignore b/.gitignore index b0e1ac71ee..86b6e0d63d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ bin # Release artifacts dist bundle.tar.gz +vendor # editor and IDE paraphernalia .idea diff --git a/pkg/collector/reconcile/configmap.go b/pkg/collector/reconcile/configmap.go index 0b07db9689..82d57b5393 100644 --- a/pkg/collector/reconcile/configmap.go +++ b/pkg/collector/reconcile/configmap.go @@ -120,7 +120,7 @@ func expectedConfigMaps(ctx context.Context, params Params, expected []corev1.Co updated.ObjectMeta.Labels[k] = v } - patch := client.MergeFrom(¶ms.Instance) + patch := client.MergeFrom(existing) if err := params.Client.Patch(ctx, updated, patch); err != nil { return fmt.Errorf("failed to apply changes: %w", err) diff --git a/pkg/collector/reconcile/configmap_test.go b/pkg/collector/reconcile/configmap_test.go new file mode 100644 index 0000000000..7eaee37fc5 --- /dev/null +++ b/pkg/collector/reconcile/configmap_test.go @@ -0,0 +1,141 @@ +// Copyright The OpenTelemetry 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 reconcile + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + + "github.com/open-telemetry/opentelemetry-operator/api/v1alpha1" + "github.com/open-telemetry/opentelemetry-operator/internal/config" +) + +func TestDesiredConfigMap(t *testing.T) { + t.Run("should return expected config map", func(t *testing.T) { + expectedLables := map[string]string{ + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/name": "test-collector", + } + + expectedData := map[string]string{ + "collector.yaml": ` + receivers: + jaeger: + protocols: + grpc: + processors: + + exporters: + logging: + + service: + pipelines: + traces: + receivers: [jaeger] + processors: [] + exporters: [logging] + +`, + } + + actual := desiredConfigMap(context.Background(), params()) + + assert.Equal(t, "test-collector", actual.Name) + assert.Equal(t, expectedLables, actual.Labels) + assert.Equal(t, expectedData, actual.Data) + + }) + +} + +func TestExpectedConfigMap(t *testing.T) { + t.Run("should create config map", func(t *testing.T) { + err := expectedConfigMaps(context.Background(), params(), []v1.ConfigMap{desiredConfigMap(context.Background(), params())}, true) + assert.NoError(t, err) + + exists, err := populateObjectIfExists(t, &v1.ConfigMap{}, types.NamespacedName{Namespace: "default", Name: "test-collector"}) + + assert.NoError(t, err) + assert.True(t, exists) + }) + + t.Run("should update config map", func(t *testing.T) { + + param := Params{ + Config: config.New(), + Client: k8sClient, + Instance: v1alpha1.OpenTelemetryCollector{ + TypeMeta: metav1.TypeMeta{ + Kind: "opentelemetry.io", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + }, + Scheme: testScheme, + Log: logger, + Recorder: record.NewFakeRecorder(10), + } + cm := desiredConfigMap(context.Background(), param) + createObjectIfNotExists(t, "test-collector", &cm) + + err := expectedConfigMaps(context.Background(), params(), []v1.ConfigMap{desiredConfigMap(context.Background(), params())}, true) + assert.NoError(t, err) + + actual := v1.ConfigMap{} + exists, err := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "test-collector"}) + + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, instanceUID, actual.OwnerReferences[0].UID) + assert.Equal(t, params().Instance.Spec.Config, actual.Data["collector.yaml"]) + }) + + t.Run("should delete config map", func(t *testing.T) { + + deletecm := v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-delete-collector", + Namespace: "default", + Labels: map[string]string{ + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + }, + }, + } + createObjectIfNotExists(t, "test-delete-collector", &deletecm) + + exists, _ := populateObjectIfExists(t, &v1.ConfigMap{}, types.NamespacedName{Namespace: "default", Name: "test-delete-collector"}) + assert.True(t, exists) + + err := deleteConfigMaps(context.Background(), params(), []v1.ConfigMap{desiredConfigMap(context.Background(), params())}) + assert.NoError(t, err) + + exists, _ = populateObjectIfExists(t, &v1.ConfigMap{}, types.NamespacedName{Namespace: "default", Name: "test-delete-collector"}) + assert.False(t, exists) + }) +} diff --git a/pkg/collector/reconcile/daemonset.go b/pkg/collector/reconcile/daemonset.go index 8b01af432d..fa6a1a6f82 100644 --- a/pkg/collector/reconcile/daemonset.go +++ b/pkg/collector/reconcile/daemonset.go @@ -89,7 +89,7 @@ func expectedDaemonSets(ctx context.Context, params Params, expected []appsv1.Da updated.ObjectMeta.Labels[k] = v } - patch := client.MergeFrom(¶ms.Instance) + patch := client.MergeFrom(existing) if err := params.Client.Patch(ctx, updated, patch); err != nil { return fmt.Errorf("failed to apply changes: %w", err) } diff --git a/pkg/collector/reconcile/daemonset_test.go b/pkg/collector/reconcile/daemonset_test.go new file mode 100644 index 0000000000..8a4751e8b6 --- /dev/null +++ b/pkg/collector/reconcile/daemonset_test.go @@ -0,0 +1,133 @@ +// Copyright The OpenTelemetry 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 reconcile + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/open-telemetry/opentelemetry-operator/pkg/collector" +) + +func TestExpectedDaemonsets(t *testing.T) { + param := params() + expectedDs := collector.DaemonSet(param.Config, logger, param.Instance) + + t.Run("should create Daemonset", func(t *testing.T) { + err := expectedDaemonSets(context.Background(), param, []v1.DaemonSet{expectedDs}) + assert.NoError(t, err) + + exists, err := populateObjectIfExists(t, &v1.DaemonSet{}, types.NamespacedName{Namespace: "default", Name: "test-collector"}) + + assert.NoError(t, err) + assert.True(t, exists) + + }) + t.Run("should update Daemonset", func(t *testing.T) { + createObjectIfNotExists(t, "test-collector", &expectedDs) + err := expectedDaemonSets(context.Background(), param, []v1.DaemonSet{expectedDs}) + assert.NoError(t, err) + + actual := v1.DaemonSet{} + exists, err := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "test-collector"}) + + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, instanceUID, actual.OwnerReferences[0].UID) + }) + + t.Run("should delete daemonset", func(t *testing.T) { + + labels := map[string]string{ + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + } + ds := v1.DaemonSet{} + ds.Name = "dummy" + ds.Namespace = "default" + ds.Labels = labels + ds.Spec = v1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "dummy", + Image: "busybox", + }}, + }, + }, + } + + createObjectIfNotExists(t, "dummy", &ds) + + err := deleteDaemonSets(context.Background(), param, []v1.DaemonSet{expectedDs}) + assert.NoError(t, err) + + actual := v1.DaemonSet{} + exists, _ := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "dummy"}) + + assert.False(t, exists) + + }) + + t.Run("should not delete daemonset", func(t *testing.T) { + + labels := map[string]string{ + "app.kubernetes.io/managed-by": "helm-opentelemetry-operator", + } + ds := v1.DaemonSet{} + ds.Name = "dummy" + ds.Namespace = "default" + ds.Labels = labels + ds.Spec = v1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "dummy", + Image: "busybox", + }}, + }, + }, + } + + createObjectIfNotExists(t, "dummy", &ds) + + err := deleteDaemonSets(context.Background(), param, []v1.DaemonSet{expectedDs}) + assert.NoError(t, err) + + actual := v1.DaemonSet{} + exists, _ := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "dummy"}) + + assert.True(t, exists) + + }) +} diff --git a/pkg/collector/reconcile/deployment.go b/pkg/collector/reconcile/deployment.go index 399b74e800..41b0f0de65 100644 --- a/pkg/collector/reconcile/deployment.go +++ b/pkg/collector/reconcile/deployment.go @@ -89,7 +89,7 @@ func expectedDeployments(ctx context.Context, params Params, expected []appsv1.D updated.ObjectMeta.Labels[k] = v } - patch := client.MergeFrom(¶ms.Instance) + patch := client.MergeFrom(existing) if err := params.Client.Patch(ctx, updated, patch); err != nil { return fmt.Errorf("failed to apply changes: %w", err) diff --git a/pkg/collector/reconcile/deployment_test.go b/pkg/collector/reconcile/deployment_test.go new file mode 100644 index 0000000000..7295ef05fc --- /dev/null +++ b/pkg/collector/reconcile/deployment_test.go @@ -0,0 +1,130 @@ +// Copyright The OpenTelemetry 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 reconcile + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/open-telemetry/opentelemetry-operator/pkg/collector" +) + +func TestExpectedDeployments(t *testing.T) { + param := params() + expectedDeploy := collector.Deployment(param.Config, logger, param.Instance) + + t.Run("should create deployment", func(t *testing.T) { + err := expectedDeployments(context.Background(), param, []v1.Deployment{expectedDeploy}) + assert.NoError(t, err) + + exists, err := populateObjectIfExists(t, &v1.Deployment{}, types.NamespacedName{Namespace: "default", Name: "test-collector"}) + + assert.NoError(t, err) + assert.True(t, exists) + + }) + t.Run("should update deployment", func(t *testing.T) { + createObjectIfNotExists(t, "test-collector", &expectedDeploy) + err := expectedDeployments(context.Background(), param, []v1.Deployment{expectedDeploy}) + assert.NoError(t, err) + + actual := v1.Deployment{} + exists, err := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "test-collector"}) + + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, instanceUID, actual.OwnerReferences[0].UID) + assert.Equal(t, int32(2), *actual.Spec.Replicas) + }) + + t.Run("should delete deployment", func(t *testing.T) { + labels := map[string]string{ + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + } + deploy := v1.Deployment{} + deploy.Name = "dummy" + deploy.Namespace = "default" + deploy.Labels = labels + deploy.Spec = v1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "dummy", + Image: "busybox", + }}, + }, + }, + } + createObjectIfNotExists(t, "dummy", &deploy) + + err := deleteDeployments(context.Background(), param, []v1.Deployment{expectedDeploy}) + assert.NoError(t, err) + + actual := v1.Deployment{} + exists, _ := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "dummy"}) + + assert.False(t, exists) + + }) + + t.Run("should not delete deployment", func(t *testing.T) { + labels := map[string]string{ + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/managed-by": "helm-opentelemetry-operator", + } + deploy := v1.Deployment{} + deploy.Name = "dummy" + deploy.Namespace = "default" + deploy.Spec = v1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "dummy", + Image: "busybox", + }}, + }, + }, + } + createObjectIfNotExists(t, "dummy", &deploy) + + err := deleteDeployments(context.Background(), param, []v1.Deployment{expectedDeploy}) + assert.NoError(t, err) + + actual := v1.Deployment{} + exists, _ := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "dummy"}) + + assert.True(t, exists) + + }) +} diff --git a/pkg/collector/reconcile/opentelemetry_test.go b/pkg/collector/reconcile/opentelemetry_test.go new file mode 100644 index 0000000000..50c08d78e1 --- /dev/null +++ b/pkg/collector/reconcile/opentelemetry_test.go @@ -0,0 +1,42 @@ +// Copyright The OpenTelemetry 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 reconcile + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/types" + + "github.com/open-telemetry/opentelemetry-operator/api/v1alpha1" +) + +func TestSelf(t *testing.T) { + t.Run("should add version to the status", func(t *testing.T) { + instance := params().Instance + createObjectIfNotExists(t, "test", &instance) + err := Self(context.Background(), params()) + assert.NoError(t, err) + + actual := v1alpha1.OpenTelemetryCollector{} + exists, err := populateObjectIfExists(t, &actual, types.NamespacedName{Namespace: "default", Name: "test"}) + assert.NoError(t, err) + assert.True(t, exists) + + assert.Equal(t, actual.Status.Version, "0.0.0") + + }) +} diff --git a/pkg/collector/reconcile/suite_test.go b/pkg/collector/reconcile/suite_test.go new file mode 100644 index 0000000000..af41e0b398 --- /dev/null +++ b/pkg/collector/reconcile/suite_test.go @@ -0,0 +1,145 @@ +// Copyright The OpenTelemetry 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 reconcile + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/open-telemetry/opentelemetry-operator/api/v1alpha1" + "github.com/open-telemetry/opentelemetry-operator/internal/config" +) + +var k8sClient client.Client +var testEnv *envtest.Environment +var testScheme *runtime.Scheme = scheme.Scheme +var logger = logf.Log.WithName("unit-tests") + +var instanceUID = uuid.NewUUID() + +func TestMain(m *testing.M) { + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + } + + cfg, err := testEnv.Start() + if err != nil { + fmt.Printf("failed to start testEnv: %v", err) + os.Exit(1) + } + + if err := v1alpha1.AddToScheme(testScheme); err != nil { + fmt.Printf("failed to register scheme: %v", err) + os.Exit(1) + } + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + if err != nil { + fmt.Printf("failed to setup a Kubernetes client: %v", err) + os.Exit(1) + } + + code := m.Run() + + err = testEnv.Stop() + if err != nil { + fmt.Printf("failed to stop testEnv: %v", err) + os.Exit(1) + } + + os.Exit(code) +} + +func params() Params { + replicas := int32(2) + return Params{ + Config: config.New(), + Client: k8sClient, + Instance: v1alpha1.OpenTelemetryCollector{ + TypeMeta: metav1.TypeMeta{ + Kind: "opentelemetry.io", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.OpenTelemetryCollectorSpec{ + Replicas: &replicas, + Config: ` + receivers: + jaeger: + protocols: + grpc: + processors: + + exporters: + logging: + + service: + pipelines: + traces: + receivers: [jaeger] + processors: [] + exporters: [logging] + +`, + }, + }, + Scheme: testScheme, + Log: logger, + Recorder: record.NewFakeRecorder(10), + } +} + +func createObjectIfNotExists(tb testing.TB, name string, object client.Object) { + tb.Helper() + err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: "default", Name: name}, object) + if errors.IsNotFound(err) { + err := k8sClient.Create(context.Background(), + object) + assert.NoError(tb, err) + } +} + +func populateObjectIfExists(t testing.TB, object client.Object, namespacedName types.NamespacedName) (bool, error) { + t.Helper() + err := k8sClient.Get(context.Background(), namespacedName, object) + if errors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + +}