diff --git a/.github/workflows/operator-integration-test.yml b/.github/workflows/operator-integration-test.yml index d46630e11..69619f60a 100644 --- a/.github/workflows/operator-integration-test.yml +++ b/.github/workflows/operator-integration-test.yml @@ -22,6 +22,7 @@ permissions: jobs: InstrumentationTest: name: InstrumentationTest + needs: [BuildAndCacheImage] runs-on: ubuntu-latest permissions: id-token: write @@ -31,7 +32,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - + - uses: actions/setup-go@v5 - name: Start minikube uses: medyagh/setup-minikube@master @@ -44,11 +45,17 @@ jobs: sleep 10 kubectl get pods -A - - name: Build image + - name: Restore cached Docker image + uses: actions/cache@v3 + with: + path: image.tar + key: docker-image-${{ github.sha }} + + - name: Load Docker image run: | eval $(minikube docker-env) - make container - docker images + docker load < image.tar + - name: Deploy operator to minikube run: | @@ -78,6 +85,7 @@ jobs: kubectl wait --for=condition=Ready pod --all -n default kubectl get pods -A kubectl describe pods -n default + kubectl logs deployment/cloudwatch-controller-manager --all-containers=true -n amazon-cloudwatch go run integration-tests/manifests/cmd/validate_instrumentation_vars.go default integration-tests/java/default_instrumentation_java_env_variables.json app_signals - name: Test for defined instrumentation resources for Java @@ -230,6 +238,7 @@ jobs: DeploymentAnnotationsTest: name: DeploymentAnnotationsTest + needs: [BuildAndCacheImage] runs-on: ubuntu-latest permissions: id-token: write @@ -239,6 +248,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-go@v5 - name: Start minikube uses: medyagh/setup-minikube@master @@ -252,11 +262,16 @@ jobs: sleep 10 kubectl get pods -A - - name: Build image + - name: Restore cached Docker image + uses: actions/cache@v3 + with: + path: image.tar + key: docker-image-${{ github.sha }} + + - name: Load Docker image run: | eval $(minikube docker-env) - make container - docker images + docker load < image.tar - name: Deploy operator to minikube run: | @@ -280,9 +295,97 @@ jobs: go test -v -run TestNodeJSOnlyDeployment ./integration-tests/manifests/annotations -timeout 30m sleep 5 go test -v -run TestAnnotationsOnMultipleResources ./integration-tests/manifests/annotations -timeout 30m + DefineAutoMonitorTestMatrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v5 + - name: Generate test list + id: tests + run: | + cd integration-tests/manifests/automonitor/ && \ + echo "tests<> $GITHUB_OUTPUT + go test -list . | sed -e '$d' >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - name: Create test matrix + id: set-matrix + run: | + TESTS=$(echo "${{ steps.tests.outputs.tests }}" | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "matrix={\"test\":${TESTS}}" >> $GITHUB_OUTPUT + BuildAndCacheImage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + - name: Build image + run: | + make container + docker save $(docker images --format '{{.Repository}}:{{.Tag}}' | grep operator | head -n 1) > image.tar + + - name: Cache Docker image + uses: actions/cache@v3 + with: + path: image.tar + key: docker-image-${{ github.sha }} + + AutoMonitorTest: + name: AutoMonitorTest + runs-on: ubuntu-latest + needs: [DefineAutoMonitorTestMatrix, BuildAndCacheImage] + permissions: + id-token: write + contents: read + strategy: + matrix: ${{fromJson(needs.DefineAutoMonitorTestMatrix.outputs.matrix)}} + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + + - name: Start minikube + uses: medyagh/setup-minikube@master + + - name: Deploy cert-manager to minikube + run: + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.0/cert-manager.yaml + + - name: Verify minikube and cert-manager + run: | + sleep 10 + kubectl get pods -A + + - name: Restore cached Docker image + uses: actions/cache@v3 + with: + path: image.tar + key: docker-image-${{ github.sha }} + + - name: Load Docker image + run: | + eval $(minikube docker-env) + docker load < image.tar + + - name: Deploy operator to minikube + run: | + make deploy + + - name: Test AutoMonitor-created annotations + run: | + kubectl get pods -A + kubectl describe pods -n default + go test -v ./integration-tests/manifests/automonitor -run ^${{ matrix.test }}$ -timeout 60m -controllerManagerName 'cloudwatch-controller-manager' DaemonsetAnnotationsTest: name: DaemonsetAnnotationsTest + needs: [BuildAndCacheImage] runs-on: ubuntu-latest permissions: id-token: write @@ -292,6 +395,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-go@v5 - name: Start minikube uses: medyagh/setup-minikube@master @@ -305,11 +409,17 @@ jobs: sleep 10 kubectl get pods -A - - name: Build image + - name: Restore cached Docker image + uses: actions/cache@v3 + with: + path: image.tar + key: docker-image-${{ github.sha }} + + - name: Load Docker image run: | eval $(minikube docker-env) - make container - docker images + docker load < image.tar + - name: Deploy operator to minikube run: | @@ -336,6 +446,7 @@ jobs: StatefulsetAnnotationsTest: name: StatefulsetAnnotationsTest + needs: [BuildAndCacheImage] runs-on: ubuntu-latest permissions: id-token: write @@ -345,6 +456,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-go@v5 - name: Start minikube uses: medyagh/setup-minikube@master @@ -358,11 +470,17 @@ jobs: sleep 10 kubectl get pods -A - - name: Build image + - name: Restore cached Docker image + uses: actions/cache@v3 + with: + path: image.tar + key: docker-image-${{ github.sha }} + + - name: Load Docker image run: | eval $(minikube docker-env) - make container - docker images + docker load < image.tar + - name: Deploy operator to minikube run: | @@ -390,6 +508,7 @@ jobs: NamespaceAnnotationsTest: name: NamespaceAnnotationsTest + needs: [BuildAndCacheImage] runs-on: ubuntu-latest permissions: id-token: write @@ -399,6 +518,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-go@v5 - name: Start minikube uses: medyagh/setup-minikube@master @@ -411,11 +531,17 @@ jobs: run: | kubectl get pods -A - - name: Build image + - name: Restore cached Docker image + uses: actions/cache@v3 + with: + path: image.tar + key: docker-image-${{ github.sha }} + + - name: Load Docker image run: | eval $(minikube docker-env) - make container - docker images + docker load < image.tar + - name: Deploy operator to minikube run: | diff --git a/integration-tests/manifests/admin-dashboard.yaml b/integration-tests/manifests/admin-dashboard.yaml new file mode 100644 index 000000000..eb69de81c --- /dev/null +++ b/integration-tests/manifests/admin-dashboard.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: admin-dashboard + labels: + app: admin-dashboard +spec: + replicas: 1 + selector: + matchLabels: + app: admin-dashboard + template: + metadata: + labels: + app: admin-dashboard + spec: + containers: + - name: admin-dashboard-container + image: nginx:latest + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: admin-dashboard-service +spec: + selector: + app: admin-dashboard + ports: + - port: 80 + targetPort: 80 diff --git a/integration-tests/manifests/automonitor/validate_automonitor_methods.go b/integration-tests/manifests/automonitor/validate_automonitor_methods.go new file mode 100644 index 000000000..aa2b3081b --- /dev/null +++ b/integration-tests/manifests/automonitor/validate_automonitor_methods.go @@ -0,0 +1,573 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package annotations + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strconv" + "strings" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/go-logr/logr/testr" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" + + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation/auto" + + appsV1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + + "github.com/aws/amazon-cloudwatch-agent-operator/integration-tests/util" +) + +type workloadType string + +const ( + Deployment workloadType = "deployment" + DaemonSet workloadType = "daemonset" + StatefulSet workloadType = "statefulset" + + injectJavaAnnotation = "instrumentation.opentelemetry.io/inject-java" + autoAnnotateJavaAnnotation = "cloudwatch.aws.amazon.com/auto-annotate-java" + injectPythonAnnotation = "instrumentation.opentelemetry.io/inject-python" + autoAnnotatePythonAnnotation = "cloudwatch.aws.amazon.com/auto-annotate-python" + injectDotNetAnnotation = "instrumentation.opentelemetry.io/inject-dotnet" + autoAnnotateDotNetAnnotation = "cloudwatch.aws.amazon.com/auto-annotate-dotnet" + injectNodeJSAnnotation = "instrumentation.opentelemetry.io/inject-nodejs" + autoAnnotateNodeJSAnnotation = "cloudwatch.aws.amazon.com/auto-annotate-nodejs" + + kubeSystemNamespace = "kube-system" + amazonCloudwatchNamespace = "amazon-cloudwatch" + + timoutDuration = 2 * time.Minute + timeBetweenRetries = 5 * time.Second +) + +var ( + amazonControllerManager = flag.String("controllerManagerName", "cloudwatch-controller-manager", "short") +) + +type TestHelper struct { + clientSet *kubernetes.Clientset + t *testing.T + startTime time.Time + logger logr.Logger +} + +func NewTestHelper(t *testing.T) *TestHelper { + logger := testr.New(t) + return &TestHelper{ + clientSet: setupTest(t, logger), + t: t, + logger: logger, + } +} + +func (h *TestHelper) Initialize(namespace string) string { + newUUID := uuid.New() + uniqueNamespace := fmt.Sprintf("%s-%s", namespace, newUUID.String()) + + h.UpdateMonitorConfig(&auto.MonitorConfig{MonitorAllServices: false, RestartPods: false}) + h.UpdateAnnotationConfig(nil) + h.startTime = time.Now() + + return uniqueNamespace +} + +func setupTest(t *testing.T, logger logr.Logger) *kubernetes.Clientset { + userHomeDir, err := os.UserHomeDir() + + if err != nil { + t.Errorf("error getting user home dir: %v\n\n", err) + } + kubeConfigPath := filepath.Join(userHomeDir, ".kube", "config") + logger.Info(fmt.Sprintf("Using kubeconfig: %s\n\n", kubeConfigPath)) + + kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) + if err != nil { + t.Errorf("Error getting kubernetes config: %v\n\n", err) + } + + clientSet, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + t.Errorf("error getting kubernetes config: %v\n\n", err) + } + return clientSet +} + +func (h *TestHelper) ApplyYAMLWithKubectl(filename, namespace string) error { + cmd := exec.Command("kubectl", "apply", "--wait=true", "-f", filename, "-n", namespace) + h.logger.Info(fmt.Sprintf("Applying YAML with kubectl %s\n", cmd)) + return cmd.Run() +} + +func (h *TestHelper) CreateNamespaceAndApplyResources(namespace string, resourceFiles []string, skipDelete bool) error { + h.logger.Info(fmt.Sprintf("Creating namespace %s\n", namespace)) + err := h.CreateNamespace(namespace, skipDelete) + if err != nil { + return err + } + + for _, file := range resourceFiles { + err = h.ApplyYAMLWithKubectl(file, namespace) + if err != nil { + h.t.Errorf("Could not apply resources %s/%s\n", namespace, file) + return err + } + } + + if !skipDelete { + h.t.Cleanup(func() { + h.logger.Info(fmt.Sprintf("Deleting resources %s in namespace %s", namespace, resourceFiles)) + if err := h.DeleteResources(namespace, resourceFiles); err != nil { + h.t.Logf("Failed to delete resources: %v", err) + } + }) + } + return nil +} + +func (h *TestHelper) IsNamespaceUpdated(namespace string) bool { + ns, err := h.clientSet.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) + if err != nil { + h.logger.Info(fmt.Sprintf("Failed to get namespace %s: %v\n", namespace, err)) + return false + } + return ns.CreationTimestamp.After(h.startTime) || ns.ResourceVersion != "" +} + +func (h *TestHelper) DeleteYAMLWithKubectl(filename, namespace string) error { + cmd := exec.Command("kubectl", "delete", "-f", filename, "-n", namespace) + return cmd.Run() +} + +func (h *TestHelper) DeleteResources(name string, resourceFiles []string) error { + for _, file := range resourceFiles { + if err := h.DeleteYAMLWithKubectl(file, name); err != nil { + return err + } + } + return nil +} + +// CreateNamespace if not already created +func (h *TestHelper) CreateNamespace(name string, skipDelete bool) error { + _, err := h.clientSet.CoreV1().Namespaces().Get(context.Background(), name, metav1.GetOptions{}) + if err == nil { + return nil + } else if !errors.IsNotFound(err) { + return err + } + + namespace := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}} + _, err = h.clientSet.CoreV1().Namespaces().Create(context.Background(), namespace, metav1.CreateOptions{}) + if err != nil { + return err + } + + startTime := time.Now() + for { + if time.Since(startTime) > timoutDuration { + return fmt.Errorf("timeout reached while waiting for namespace %s to be created", name) + } + + _, err := h.clientSet.CoreV1().Namespaces().Get(context.Background(), name, metav1.GetOptions{}) + if err == nil { + break + } else if !errors.IsNotFound(err) { + return err + } + + time.Sleep(timeBetweenRetries) + } + + if !skipDelete { + h.t.Cleanup(func() { + h.logger.Info(fmt.Sprintf("Deleting namespace %v", namespace)) + if err := h.DeleteNamespace(name); err != nil { + h.t.Fatalf("Failed to delete namespaces: %v", err) + } + }) + } + return nil +} + +func (h *TestHelper) DeleteNamespace(name string) error { + return h.clientSet.CoreV1().Namespaces().Delete(context.Background(), name, metav1.DeleteOptions{}) +} + +func (h *TestHelper) UpdateOperator(deployment *appsV1.Deployment) bool { + args := deployment.Spec.Template.Spec.Containers[0].Args + now := time.Now() + + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Get the latest version of the deployment + currentDeployment, err := h.clientSet.AppsV1().Deployments(amazonCloudwatchNamespace).Get(context.TODO(), *amazonControllerManager, metav1.GetOptions{}) + if err != nil { + return err + } + + // Apply your changes to the latest version + currentDeployment.Spec.Template.Spec.Containers[0].Args = args + forceRestart(currentDeployment) + + // Try to update + _, updateErr := h.clientSet.AppsV1().Deployments(amazonCloudwatchNamespace).Update(context.TODO(), currentDeployment, metav1.UpdateOptions{}) + return updateErr + }) + + if retryErr != nil { + h.t.Errorf("Failed to update deployment after retries: %v\n", retryErr) + return false + } + + err := util.WaitForNewPodCreation(h.clientSet, deployment, now) + if err != nil { + h.logger.Error(err, "There was an error trying to wait for deployment available") + return false + } + + h.logger.Info("Operator updated successfully!", "args", args) + return true +} + +func forceRestart(deployment *appsV1.Deployment) { + annotations := deployment.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations["test-restart"] = time.Now().String() + deployment.Spec.Template.SetAnnotations(annotations) +} + +func (h *TestHelper) findIndexOfPrefix(str string, strs []string) int { + for i, s := range strs { + if strings.HasPrefix(s, str) { + return i + } + } + return -1 +} + +func (h *TestHelper) UpdateMonitorConfig(config *auto.MonitorConfig) { + jsonStr, err := json.Marshal(config) + assert.Nil(h.t, err) + + h.logger.Info("Setting monitor config to:") + h.updateOperatorConfig(string(jsonStr), "--auto-monitor-config=") +} + +func (h *TestHelper) UpdateAnnotationConfig(config *auto.AnnotationConfig) { + var jsonStr = "" + if marshalledConfig, err := json.Marshal(config); config != nil && assert.Nil(h.t, err) { + jsonStr = string(marshalledConfig) + } + h.logger.Info("Setting annotation config to ", "jsonStr", jsonStr) + h.updateOperatorConfig(jsonStr, "--auto-annotation-config=") +} + +func (h *TestHelper) updateOperatorConfig(jsonStr string, flag string) { + deployment, err := h.clientSet.AppsV1().Deployments(amazonCloudwatchNamespace).Get(context.TODO(), *amazonControllerManager, metav1.GetOptions{}) + if err != nil { + h.t.Errorf("Error getting deployment: %v\n\n", err) + return + } + args := deployment.Spec.Template.Spec.Containers[0].Args + indexOfAutoAnnotationConfigString := h.findIndexOfPrefix(flag, args) + shouldDelete := len(jsonStr) == 0 + if indexOfAutoAnnotationConfigString < 0 { + if !shouldDelete { + deployment.Spec.Template.Spec.Containers[0].Args = append(deployment.Spec.Template.Spec.Containers[0].Args, flag+jsonStr) + } + } else { + if shouldDelete { + deployment.Spec.Template.Spec.Containers[0].Args = slices.Delete(deployment.Spec.Template.Spec.Containers[0].Args, indexOfAutoAnnotationConfigString, indexOfAutoAnnotationConfigString+1) + } else { + deployment.Spec.Template.Spec.Containers[0].Args[indexOfAutoAnnotationConfigString] = flag + jsonStr + } + } + + if !h.UpdateOperator(deployment) { + h.t.Error("Failed to update Operator", deployment, deployment.Name, deployment.Spec.Template.Spec.Containers[0].Args) + } + time.Sleep(5 * time.Second) +} + +func (h *TestHelper) ValidateNamespaceAnnotations(namespace string, shouldExist []string, shouldNotExist []string) error { + for { + if h.IsNamespaceUpdated(namespace) { + h.logger.Info(fmt.Sprintf("Namespace %s has been updated.\n", namespace)) + break + } + elapsed := time.Since(h.startTime) + if elapsed >= timoutDuration { + h.logger.Info(fmt.Sprintf("Timeout reached while waiting for namespace %s to be updated.\n", namespace)) + break + } + } + + maxRetries := 3 + var annotations map[string]string + var err error + var ns *v1.Namespace + for attempt := 0; attempt < maxRetries; attempt++ { + if attempt > 0 { + h.logger.Info(fmt.Sprintf("Attempt %d/%d: Waiting 5 seconds before retrying...", attempt+1, maxRetries)) + time.Sleep(timeBetweenRetries) + } + + ns, err = h.clientSet.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) + if err != nil { + h.logger.Error(err, "There was an error getting namespace") + return err + } + + annotations = ns.ObjectMeta.Annotations + if annotations != nil && len(annotations) > 0 { + break + } + + h.logger.Info(fmt.Sprintf("Namespace annotations are empty or nil on attempt %d/%d", attempt+1, maxRetries)) + + if attempt == maxRetries-1 { + annotations = map[string]string{} + } + } + + for _, shouldExistAnnotation := range shouldExist { + if _, ok := annotations[shouldExistAnnotation]; !ok { + return fmt.Errorf("annotation should be present: %s", shouldExistAnnotation) + } + } + + for _, shouldNotExistAnnotation := range shouldNotExist { + if _, ok := annotations[shouldNotExistAnnotation]; ok { + return fmt.Errorf("annotation should not be present: %s", shouldNotExistAnnotation) + } + } + + return nil +} + +func (h *TestHelper) ValidateWorkloadAnnotations(workloadType workloadType, namespace, resourceName string, shouldExist []string, shouldNotExist []string) error { + return retry.OnError(retry.DefaultBackoff, func(err error) bool { + return err != nil + }, func() error { + var annotations map[string]string + switch workloadType { + case Deployment: + resource, err := h.clientSet.AppsV1().Deployments(namespace).Get(context.TODO(), resourceName, metav1.GetOptions{}) + if err != nil { + return err + } + annotations = resource.Spec.Template.Annotations + case DaemonSet: + resource, err := h.clientSet.AppsV1().DaemonSets(namespace).Get(context.TODO(), resourceName, metav1.GetOptions{}) + if err != nil { + return err + } + annotations = resource.Spec.Template.Annotations + case StatefulSet: + resource, err := h.clientSet.AppsV1().StatefulSets(namespace).Get(context.TODO(), resourceName, metav1.GetOptions{}) + if err != nil { + return err + } + annotations = resource.Spec.Template.Annotations + default: + return fmt.Errorf("unsupported resource type: %s", workloadType) + } + + // resource level annotation validation + if len(annotations) > 0 { + for _, shouldExistAnnotation := range shouldExist { + if _, ok := annotations[shouldExistAnnotation]; !ok { + return fmt.Errorf("annotation should be present: %s", shouldExistAnnotation) + } + } + for _, shouldNotExistAnnotation := range shouldNotExist { + if _, ok := annotations[shouldNotExistAnnotation]; ok { + return fmt.Errorf("annotation should not be present: %s", shouldNotExistAnnotation) + } + } + } + return nil + }) +} + +func (h *TestHelper) ValidatePodsAnnotations(namespace string, shouldExist []string, shouldNotExist []string) error { + currentPods, err := h.clientSet.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + h.logger.Info(fmt.Sprintf("Failed to list pods: %v\n", err)) + return err + } + for _, pod := range currentPods.Items { + h.logger.Info(fmt.Sprintf("Pod %s is in phase %s\n", pod.Name, pod.Status.Phase)) + if pod.Status.Phase != v1.PodRunning { + continue + } + annotations := pod.Annotations + for _, shouldExistAnnotation := range shouldExist { + if _, ok := annotations[shouldExistAnnotation]; !ok { + return fmt.Errorf("annotation should be present: %s", shouldExistAnnotation) + } + } + for _, shouldNotExistAnnotation := range shouldNotExist { + if _, ok := annotations[shouldNotExistAnnotation]; ok { + return fmt.Errorf("annotation should not be present: %s", shouldNotExistAnnotation) + } + } + } + + return nil +} + +func (h *TestHelper) NumberOfRevisions(deploymentName string, namespace string) int { + numOfRevisions := 0 + i := 0 + for { + cmd := exec.Command("kubectl", "rollout", "history", "deployment", deploymentName, "-n", namespace, "--revision", strconv.Itoa(i)) + if err := cmd.Run(); err != nil { + break + } + numOfRevisions++ + i++ + } + return numOfRevisions - 1 +} + +func (h *TestHelper) WaitYamlWithKubectl(filename string, namespace string) error { + cmd := exec.Command("kubectl", "wait", "--for=create", "-f", filename, "-n", namespace) + h.logger.Info(fmt.Sprintf("Waiting YAML with kubectl %s\n", cmd)) + return cmd.Run() +} + +func (h *TestHelper) RolloutWaitYamlWithKubectl(filename string, namespace string) error { + cmd := exec.Command("kubectl", "rollout", "status", "-f", filename, "-n", namespace) + h.logger.Info(fmt.Sprintf("Waiting YAML with kubectl %s\n", cmd)) + return cmd.Run() +} + +func (h *TestHelper) RestartWorkload(wlType workloadType, namespace, name string) error { + h.logger.Info(fmt.Sprintf("Restarting %s/%s...", namespace, name)) + + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var updateErr error + + switch wlType { + case Deployment: + deployment, err := h.clientSet.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get deployment: %v", err) + } + + if deployment.Spec.Template.Annotations == nil { + deployment.Spec.Template.Annotations = make(map[string]string) + } + deployment.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + + _, updateErr = h.clientSet.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, metav1.UpdateOptions{}) + + case DaemonSet: + ds, err := h.clientSet.AppsV1().DaemonSets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get daemonset: %v", err) + } + + if ds.Spec.Template.Annotations == nil { + ds.Spec.Template.Annotations = make(map[string]string) + } + ds.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + + _, updateErr = h.clientSet.AppsV1().DaemonSets(namespace).Update(context.TODO(), ds, metav1.UpdateOptions{}) + + case StatefulSet: + ss, err := h.clientSet.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get statefulset: %v", err) + } + + if ss.Spec.Template.Annotations == nil { + ss.Spec.Template.Annotations = make(map[string]string) + } + ss.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + + _, updateErr = h.clientSet.AppsV1().StatefulSets(namespace).Update(context.TODO(), ss, metav1.UpdateOptions{}) + + } + + if updateErr != nil { + return fmt.Errorf("failed to update %s: %v", wlType, updateErr) + } + + cmd := exec.Command("kubectl", "rollout", "status", fmt.Sprintf("%s/%s", wlType, name), "-n", namespace) + h.logger.Info(fmt.Sprintf("Waiting YAML with kubectl %s\n", cmd)) + return cmd.Run() + }) + + if retryErr != nil { + return fmt.Errorf("failed to restart %s %s: %v", wlType, name, retryErr) + } + + err := h.waitForWorkloadRollout(wlType, namespace, name) + if err != nil { + return fmt.Errorf("failed to wait for %s rollout: %v", wlType, err) + } + + h.logger.Info(fmt.Sprintf("Successfully restarted %s %s in namespace %s\n", wlType, name, namespace)) + return nil +} + +func (h *TestHelper) waitForWorkloadRollout(wlType workloadType, namespace, name string) error { + return wait.PollUntilContextTimeout( + context.TODO(), // parent context + time.Second*2, // interval between polls + time.Minute*5, // timeout + false, // immediate (set to false to match PollImmediate behavior) + func(ctx context.Context) (bool, error) { + switch wlType { + case Deployment: + deployment, err := h.clientSet.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return false, err + } + return deployment.Generation <= deployment.Status.ObservedGeneration && + deployment.Status.UpdatedReplicas == *deployment.Spec.Replicas && + deployment.Status.Replicas == *deployment.Spec.Replicas && + deployment.Status.AvailableReplicas == *deployment.Spec.Replicas, nil + + case DaemonSet: + daemonset, err := h.clientSet.AppsV1().DaemonSets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return false, err + } + return daemonset.Generation <= daemonset.Status.ObservedGeneration && + daemonset.Status.UpdatedNumberScheduled == daemonset.Status.DesiredNumberScheduled && + daemonset.Status.NumberReady == daemonset.Status.DesiredNumberScheduled, nil + case StatefulSet: + statefulset, err := h.clientSet.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return false, err + } + return statefulset.Generation <= statefulset.Status.ObservedGeneration && + statefulset.Status.UpdatedReplicas == *statefulset.Spec.Replicas && + statefulset.Status.ReadyReplicas == *statefulset.Spec.Replicas && + statefulset.Status.CurrentReplicas == *statefulset.Spec.Replicas, nil + } + return false, fmt.Errorf("unknown workload type: %s", wlType) + }) +} diff --git a/integration-tests/manifests/automonitor/validate_automonitor_test.go b/integration-tests/manifests/automonitor/validate_automonitor_test.go new file mode 100644 index 000000000..2f5334027 --- /dev/null +++ b/integration-tests/manifests/automonitor/validate_automonitor_test.go @@ -0,0 +1,1333 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package annotations + +import ( + "fmt" + "maps" + "slices" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation" + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation/auto" +) + +const ( + deploymentName = "sample-deployment" + deploymentWithoutService = "sample-deployment-without-service" + daemonSetName = "sample-daemonset" + customServiceDeploymentName = "customer-service" + statefulSetName = "sample-statefulset" + + sampleDaemonsetYaml = "../sample-daemonset.yaml" + sampleDeploymentYaml = "../sample-deployment.yaml" + sampleDeploymentServiceYaml = "../sample-deployment-service.yaml" + sampleDeploymentWithoutServiceYaml = "../sample-deployment-without-service.yaml" + sampleStatefulsetYaml = "../sample-statefulset.yaml" + customerServiceYaml = "../customer-service.yaml" + frontendAppYaml = "../frontend-app.yaml" + adminDashboardYaml = "../admin-dashboard.yaml" + conflictingDeploymentYaml = "../conflicting-deployment.yaml" +) + +var all = slices.Collect(maps.Keys(instrumentation.SupportedTypes)) +var allAnnotations = getAnnotations(all...) +var none []string + +// getAnnotations returns both auto and inject annotations for the specified language types +func getAnnotations(types ...instrumentation.Type) []string { + var annotations []string + for _, t := range types { + switch t { + case instrumentation.TypeJava: + annotations = append(annotations, autoAnnotateJavaAnnotation, injectJavaAnnotation) + case instrumentation.TypePython: + annotations = append(annotations, autoAnnotatePythonAnnotation, injectPythonAnnotation) + case instrumentation.TypeDotNet: + annotations = append(annotations, autoAnnotateDotNetAnnotation, injectDotNetAnnotation) + case instrumentation.TypeNodeJS: + annotations = append(annotations, autoAnnotateNodeJSAnnotation, injectNodeJSAnnotation) + } + } + return annotations +} + +// disabled by default +func TestDefault(t *testing.T) { + helper := NewTestHelper(t) + // copying Initialize() to skip updating operator with auto config + newUUID := uuid.New() + namespace := fmt.Sprintf("%s-%s", "test-namespace", newUUID.String()) + helper.startTime = time.Now() + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +func TestInvalidConfig(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + Languages: instrumentation.NewTypeSet("perl"), + }) + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +func TestServiceThenDeployment(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.SupportedTypes, + RestartPods: false, + }) + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) +} + +// create deployment, create service, should not annotate anything +func TestDeploymentThenServiceRestartPodsDisabled(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.SupportedTypes, + RestartPods: false, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml}, false) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + + err = helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +func TestDeploymentThenServiceRestartPodsEnabled(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.SupportedTypes, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml}, false) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + + helper.startTime = time.Now() + err = helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + time.Sleep(1 * time.Second) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) +} + +func TestDeploymentWithCustomSelector(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + // Set up custom selector config + customSelectorConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Deployments: []string{namespace + "/sample-deployment"}, + }, + Python: auto.AnnotationResources{ + Deployments: []string{namespace + "/sample-deployment"}, + }, + } + + // Update operator with auto monitor disabled and custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.SupportedTypes, + RestartPods: false, + CustomSelector: customSelectorConfig, + }) + + // Create deployment + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml}, false) + assert.NoError(t, err) + + // Validate annotations are present + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + []string{autoAnnotateJavaAnnotation, autoAnnotatePythonAnnotation}, + []string{autoAnnotateDotNetAnnotation, autoAnnotateNodeJSAnnotation}) + assert.NoError(t, err) +} + +func TestDeploymentWithCustomSelectorAfterCreation(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + // Update operator with auto monitor disabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.SupportedTypes, + RestartPods: false, + }) + + // Create deployment + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml}, false) + assert.NoError(t, err) + + // Validate no annotations present + all := allAnnotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + none, + all) + assert.NoError(t, err) + + // Update operator with custom selector + namespacedDeployment := namespace + "/sample-deployment" + customSelectorConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Deployments: []string{namespacedDeployment}, + }, + Python: auto.AnnotationResources{ + Deployments: []string{namespacedDeployment}, + }, + DotNet: auto.AnnotationResources{ + Deployments: []string{namespacedDeployment}, + }, + NodeJS: auto.AnnotationResources{ + Deployments: []string{namespacedDeployment}, + }, + } + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.SupportedTypes, + RestartPods: false, + CustomSelector: customSelectorConfig, + }) + + // Validate annotations are not present (custom selector obeys restart pods) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, all) + assert.NoError(t, err) + + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + + // validate annotations are present + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, all, none) + assert.NoError(t, err) +} + +func TestDeploymentWithExcludedThenIncludedService(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + // Set up config with exclusion + resources := auto.AnnotationResources{ + Deployments: []string{namespace + "/sample-deployment"}, + } + monitorConfig := auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.SupportedTypes, + Exclude: auto.AnnotationConfig{ + Java: resources, + Python: resources, + DotNet: resources, + NodeJS: resources, + }, + } + + // Update operator config + helper.UpdateMonitorConfig(&monitorConfig) + + // Create service first + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Create deployment + err = helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml}, false) + assert.NoError(t, err) + + // Validate that deployment has no annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + none, + allAnnotations) + assert.NoError(t, err) + + // Update config to remove exclusion + monitorConfig.Exclude = auto.AnnotationConfig{} + helper.UpdateMonitorConfig(&monitorConfig) + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + // Validate that deployment now has annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + allAnnotations, + none) + assert.NoError(t, err) +} + +// Adding more scenarios/permutations +// Permutation 1 [HIGH]: Enable monitoring for all services without auto restarts +func TestPermutation1_MonitorAllServicesNoAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: false, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify no annotations without restart + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + + // Manually restart and verify annotations + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) +} + +// Permutation 2 [HIGH]: Disable automatic monitoring for all services +func TestPermutation2_DisableMonitoringNoAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + + // First enable monitoring with auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify initial annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Disable monitoring without auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + RestartPods: false, + }) + + // Verify annotations still present + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Manually restart and verify annotations removed + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +// Permutation 3 [HIGH]: Monitor all services with pod restarts enabled +func TestPermutation3_MonitorAllServicesWithAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify no initial annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + + // Enable monitoring with auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + // Verify annotations automatically added + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) +} + +// Permutation 4 [MED]: Disable monitoring but allow pod restarts +func TestPermutation4_DisableMonitoringWithAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + + // Start with monitoring enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify initial annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Disable monitoring with auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + RestartPods: true, + }) + + // Verify annotations automatically removed + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +// Permutation 5 [HIGH]: Monitor only Java and Python services without pod restarts +func TestPermutation5_MonitorSelectedLanguagesNoAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + // Start with all languages enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify all annotations present + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Update to Java and Python only without auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: false, + }) + + // Verify annotations unchanged without restart + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Manually restart and verify only Java/Python remain + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) +} + +// Permutation 6 [MED]: Monitor Java and Python with pod restarts enabled +func TestPermutation6_MonitorSelectedLanguagesWithAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + + // Start with all languages enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify all annotations present + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Update to Java and Python only with auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: true, + }) + + // Verify only Java/Python annotations remain + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) +} + +// Permutation 7 [LOW]: Monitor Java and Python with pod restarts disabled +func TestPermutation7_MonitorSelectedLanguagesWithoutAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + + // Start with all languages enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify all annotations present + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + + // Update to Java and Python only with auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: false, + }) + + // Verify only Java/Python annotations remain + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) +} + +// Permutation 8 [LOW]: Monitor Java and Python with pod restarts enabled which should remove annotations +func TestPermutation8_MonitorSelectedLanguagesWithAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + namespace := helper.Initialize("test-namespace") + + // Start with all languages enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml}, false) + assert.NoError(t, err) + + // Verify all annotations present + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Update to Java and Python only with auto-restart + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: true, + }) + + // Verify only Java/Python annotations remain + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +// Permutation 9 [HIGH]: Monitor all services but exclude specific Java workloads +func TestPermutation9_MonitorWithExclusionsNoAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + // Set up exclusions + excludeConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + }, + Python: auto.AnnotationResources{ + Namespaces: []string{namespace}, + }, + } + + // Update config with exclusions + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: false, + Exclude: excludeConfig, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, customerServiceYaml}, false) + assert.NoError(t, err) + + // Manually restart deployments + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + err = helper.RestartWorkload(Deployment, namespace, customServiceDeploymentName) + assert.NoError(t, err) + + // Verify regular deployment has all annotations except python + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypeDotNet, instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypePython)) + assert.NoError(t, err) + + // Verify excluded customer-service has no Java annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython)) + assert.NoError(t, err) + + // Verify kube-system deployment has no Python annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypeDotNet, instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypePython)) + assert.NoError(t, err) +} + +// Permutation 10 [HIGH]: Monitor all services with auto-restarts but exclude specific Java workloads +func TestPermutation10_MonitorWithExclusionsWithAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + + // Create single namespace + namespace := helper.Initialize("test-namespace") + + // Create deployments before enabling monitoring + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, customerServiceYaml}, false) + assert.NoError(t, err) + + // Set up exclusions and enable monitoring with auto-restart + excludeConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + }, + } + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + Exclude: excludeConfig, + }) + + // Verify regular deployment has all annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Verify excluded customer-service has no Java annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython, instrumentation.TypeDotNet, instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava)) + assert.NoError(t, err) +} + +// Permutation 11 [LOW]: Disabling monitoring but keep exclusion rules +func TestPermutation11_DisableMonitorWithExclusionsNoAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + // Start with all languages enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, sampleDaemonsetYaml}, false) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Set up exclusions + excludeConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{kubeSystemNamespace}, + Deployments: []string{namespace + "/sample-deployment"}, + DaemonSets: []string{namespace + "/sample-daemonset"}, + }, + } + + // Update config with exclusions + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + RestartPods: false, + Exclude: excludeConfig, + }) + + // Manually restart deployments + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + // Verify regular deployment has no annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + + // Verify regular daemonset still has all annotations since it's not restarted + err = helper.ValidateWorkloadAnnotations(DaemonSet, namespace, "sample-daemonset", allAnnotations, none) + assert.NoError(t, err) + // Manually restart deployments + err = helper.RestartWorkload(DaemonSet, namespace, "sample-daemonset") + assert.NoError(t, err) + // Verify regular daemonset still has all annotations since it's not restarted + err = helper.ValidateWorkloadAnnotations(DaemonSet, namespace, "sample-daemonset", none, allAnnotations) + assert.NoError(t, err) +} + +// Permutation 12 [LOW]: Disabling monitoring but keep exclusion rules +func TestPermutation12_DisableMonitorWithExclusionsAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + + namespace := helper.Initialize("test-namespace") + + // Start with all languages enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, customerServiceYaml, sampleDaemonsetYaml}, false) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Set up exclusions + excludeConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{kubeSystemNamespace}, + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + }, + } + + // Update config with exclusions + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + RestartPods: true, + Exclude: excludeConfig, + }) + + // Verify regular deployment has no annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +// Permutation 13 [MED]: Disabling restart but keep exclusion rules and languages +func TestPermutation13_MonitorAndNoAutoRestartsWithLanguagesAndExclusion(t *testing.T) { + helper := NewTestHelper(t) + + // Create single namespace + namespace := helper.Initialize("test-namespace") + + // Create deployments + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, customerServiceYaml}, false) + assert.NoError(t, err) + + // Start with all languages enabled + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + //precheck + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Update config with custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: false, + Exclude: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{kubeSystemNamespace}, + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + DaemonSets: []string{namespace + "/sample-daemonset"}, + }, + }, + }) + + //postcheck with no restart + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + + // verify service associated sample-deployment + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + + // verify excluded custom-deployment + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) +} + +// Permutation 14 [MED]: monitor java and python workloads while keeping specific java workload excluded +func TestPermutation14_MonitorAndAutoRestartsWithLanguagesAndExclusion(t *testing.T) { + helper := NewTestHelper(t) + nsAnother := "another" + + // Create single namespace + namespace := helper.Initialize("test-namespace") + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, sampleDeploymentWithoutServiceYaml, customerServiceYaml}, false) + assert.NoError(t, err) + // ns deployment + err = helper.CreateNamespaceAndApplyResources(nsAnother, []string{customerServiceYaml}, false) + assert.NoError(t, err) + + // Update config with custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: true, + Exclude: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{nsAnother}, + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + DaemonSets: []string{namespace + "/" + daemonSetName}, + }, + }, + }) + + // postcheck + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentWithoutService, none, allAnnotations) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython), + getAnnotations(instrumentation.TypeJava, instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, nsAnother, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython), + getAnnotations(instrumentation.TypeJava, instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) +} + +// Permutation 15 [MED]: disable monitor while keeping specific java workload excluded +func TestPermutation15_NoMonitorAndNoAutoRestartsWithLanguagesAndExclusion(t *testing.T) { + helper := NewTestHelper(t) + + // Create single namespace + namespace := helper.Initialize("test-namespace") + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentServiceYaml, sampleDeploymentYaml, customerServiceYaml}, false) + assert.NoError(t, err) + + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + // precheck + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + + // Update config with custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: false, + Exclude: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{kubeSystemNamespace}, + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + DaemonSets: []string{namespace + "/" + daemonSetName}, + }, + }, + }) + + // postcheck + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + err = helper.RestartWorkload(Deployment, namespace, deploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + err = helper.RestartWorkload(Deployment, namespace, customServiceDeploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +// Permutation 16 [LOW]: disable monitor while keeping specific java workload excluded +func TestPermutation16_NoMonitorAndAutoRestartsWithLanguagesAndExclusion(t *testing.T) { + helper := NewTestHelper(t) + + // Create single namespace + namespace := helper.Initialize("test-namespace") + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentServiceYaml, sampleDeploymentYaml, customerServiceYaml}, false) + assert.NoError(t, err) + + // Update config with custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: true, + }) + + // precheck + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + + // Update config with custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: true, + Exclude: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{kubeSystemNamespace}, + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + DaemonSets: []string{namespace + "/" + daemonSetName}, + }, + }, + }) + + // postcheck + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, none, allAnnotations) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, none, allAnnotations) + assert.NoError(t, err) +} + +// Permutation 17 [MED]: enable monitor with no restart while adding custom selector on namespace +func TestPermutation17_MonitorAndNoAutoRestartsWithNamespaceCustomSelector(t *testing.T) { + helper := NewTestHelper(t) + + nsAnother := "another" + // Create single namespace + namespace := helper.Initialize("test-namespace") + err := helper.CreateNamespaceAndApplyResources(namespace, []string{customerServiceYaml}, false) + assert.NoError(t, err) + + // Update config with custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: false, + CustomSelector: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{nsAnother}, + }, + Python: auto.AnnotationResources{ + Namespaces: []string{nsAnother}, + }, + }, + }) + + // precheck + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, none, allAnnotations) + assert.NoError(t, err) + err = helper.RestartWorkload(Deployment, namespace, customServiceDeploymentName) + assert.NoError(t, err) + //include all after restart + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + + // postcheck + // include all + err = helper.RestartWorkload(Deployment, namespace, customServiceDeploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + // include java and python by custom selector + err = helper.CreateNamespaceAndApplyResources(nsAnother, []string{customerServiceYaml}, false) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, nsAnother, customServiceDeploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + // include java and python by custom selector + err = helper.CreateNamespaceAndApplyResources(nsAnother, []string{sampleDeploymentWithoutServiceYaml}, false) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, nsAnother, deploymentWithoutService, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + //check ns + err = helper.ValidateNamespaceAnnotations(nsAnother, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) +} + +// Permutation 18 [HIGH]: Monitor all services with customSelector and specific languages +func TestPermutation18_MonitorWithCustomSelectorAndAutoRestarts(t *testing.T) { + helper := NewTestHelper(t) + + // Create single namespace + namespace := helper.Initialize("test-namespace") + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, sampleDeploymentWithoutServiceYaml}, false) + assert.NoError(t, err) + + // Set up custom selector config + customSelectorConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{namespace}, + }, + Python: auto.AnnotationResources{ + Deployments: []string{namespace + "/sample-deployment-without-service"}, + }, + } + + // Update config with custom selector + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeDotNet), + RestartPods: true, + CustomSelector: customSelectorConfig, + }) + + // Verify service-selected deployment has dotnet annotation + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeDotNet), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + + // Verify non-service deployment has python annotation + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentWithoutService, + getAnnotations(instrumentation.TypePython), + getAnnotations(instrumentation.TypeJava, instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) +} + +// Permutation 19 [HIGH++]: Test that exclude takes precedence over all +func TestPermutation19_ConflictingCustomSelectorExclude(t *testing.T) { + helper := NewTestHelper(t) + + // Create single namespace + namespace := helper.Initialize("test-namespace") + + // Create deployments in the namespace + err := helper.CreateNamespaceAndApplyResources(namespace, []string{ + sampleDeploymentYaml, + sampleDeploymentServiceYaml, + customerServiceYaml, + conflictingDeploymentYaml, + }, false) + assert.NoError(t, err) + + // exclude config without namespace-level exclusion, will add later + excludeConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + }, + } + + customSelectorConfig := auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{namespace}, + }, + Python: auto.AnnotationResources{ + Deployments: []string{namespace + "/conflicting-deployment"}, + }, + DotNet: auto.AnnotationResources{ + Namespaces: []string{namespace}, + }, + } + + // Update operator config + monitorConfig := &auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeNodeJS), + RestartPods: true, + Exclude: excludeConfig, + CustomSelector: customSelectorConfig, + } + + helper.UpdateMonitorConfig(monitorConfig) + + // Verify conflicting-deployment has Python, NodeJS and Java + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, "conflicting-deployment", + getAnnotations(instrumentation.TypePython, instrumentation.TypeNodeJS, instrumentation.TypeJava), + getAnnotations(instrumentation.TypeDotNet)) + assert.NoError(t, err) + + // Verify customer-service has Python and NodeJS (Java excluded) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython, instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava, instrumentation.TypeDotNet)) + assert.NoError(t, err) + + // Verify sample-deployment has Java, Python, and NodeJS + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeDotNet)) + assert.NoError(t, err) +} + +// Permutation 20 [HIGH]: Disable general monitoring but enable specific instrumentation +func TestPermutation20_SelectiveMonitoringWithCustomSelector(t *testing.T) { + helper := NewTestHelper(t) + + // Create single namespace + namespace := helper.Initialize("test-namespace") + + // Create deployments + err := helper.CreateNamespaceAndApplyResources(namespace, []string{ + frontendAppYaml, + adminDashboardYaml, + sampleDeploymentYaml, + sampleDeploymentWithoutServiceYaml, + }, false) + assert.NoError(t, err) + + // Set up custom selector config + customSelectorConfig := auto.AnnotationConfig{ + Python: auto.AnnotationResources{ + Deployments: []string{namespace + "/sample-deployment-without-service"}, + }, + NodeJS: auto.AnnotationResources{ + Deployments: []string{namespace + "/frontend-app", namespace + "/admin-dashboard"}, + }, + } + + // Update operator config + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: false, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava), + RestartPods: true, + CustomSelector: customSelectorConfig, + }) + + // Verify frontend-app and admin-dashboard have NodeJS only + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, "frontend-app", + getAnnotations(instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeDotNet)) + assert.NoError(t, err) + + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, "admin-dashboard", + getAnnotations(instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeDotNet)) + assert.NoError(t, err) + + // Verify sample-deployment has no annotations + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + none, + allAnnotations) + assert.NoError(t, err) + + // Verify sample-deployment-without-service has Python + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentWithoutService, + getAnnotations(instrumentation.TypePython), + getAnnotations(instrumentation.TypeJava, instrumentation.TypeDotNet, instrumentation.TypeNodeJS)) + assert.NoError(t, err) +} + +// Permutation 21 [MED]: Enable monitoring AND No AutoRestart with granular configuration including languages, exclusion and custom selector +func TestPermutation21_SelectiveMonitoringWithCustomSelector(t *testing.T) { + helper := NewTestHelper(t) + + nsPerf := "performance-sensitive" + nsBatch := "batch-processing" + nsFinance := "finance" + nsDatabase := "database" + namespace := helper.Initialize("test-namespace") + + // Update operator config + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + }) + + // Create deployments + err := helper.CreateNamespaceAndApplyResources(namespace, []string{customerServiceYaml}, false) + assert.NoError(t, err) + // more deployments to different NSes + err = helper.CreateNamespaceAndApplyResources(nsPerf, []string{customerServiceYaml}, false) + assert.NoError(t, err) + err = helper.CreateNamespaceAndApplyResources(nsBatch, []string{customerServiceYaml}, false) + assert.NoError(t, err) + err = helper.CreateNamespaceAndApplyResources(nsFinance, []string{customerServiceYaml}, false) + assert.NoError(t, err) + err = helper.CreateNamespaceAndApplyResources(nsDatabase, []string{sampleStatefulsetYaml}, false) + assert.NoError(t, err) + + //precheck on random deployments + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(StatefulSet, nsDatabase, statefulSetName, allAnnotations, none) + assert.NoError(t, err) + + // Update operator config + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeDotNet, instrumentation.TypeNodeJS), + RestartPods: false, + CustomSelector: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{nsBatch}, + }, + }, + Exclude: auto.AnnotationConfig{ + NodeJS: auto.AnnotationResources{ + Namespaces: []string{nsPerf}, + }, + DotNet: auto.AnnotationResources{ + Deployments: []string{nsFinance + "/" + customServiceDeploymentName}, + StatefulSets: []string{nsDatabase + "/" + customServiceDeploymentName}, + }, + }, + }) + + //postcheck1 with autoRestart disabled + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(StatefulSet, nsDatabase, statefulSetName, allAnnotations, none) + assert.NoError(t, err) + + //postcheck2 ns + err = helper.ValidateNamespaceAnnotations(nsBatch, + getAnnotations(instrumentation.TypeJava), + getAnnotations(instrumentation.TypePython, instrumentation.TypeNodeJS, instrumentation.TypeDotNet)) + assert.NoError(t, err) + + //postcheck3 with manual restarts + //other ns + err = helper.RestartWorkload(Deployment, namespace, customServiceDeploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, + getAnnotations(instrumentation.TypeNodeJS, instrumentation.TypeDotNet), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython)) + assert.NoError(t, err) + // exclude nodejs by ns + err = helper.RestartWorkload(Deployment, nsPerf, customServiceDeploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, nsPerf, customServiceDeploymentName, + getAnnotations(instrumentation.TypeDotNet), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeNodeJS)) + assert.NoError(t, err) + // exclude dotnet by workloads + err = helper.RestartWorkload(Deployment, nsFinance, customServiceDeploymentName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, nsFinance, customServiceDeploymentName, + getAnnotations(instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeDotNet)) + assert.NoError(t, err) + err = helper.RestartWorkload(StatefulSet, nsDatabase, statefulSetName) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(StatefulSet, nsDatabase, statefulSetName, + getAnnotations(instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeDotNet)) + assert.NoError(t, err) + // include java only at pod level by custom selector + err = helper.RestartWorkload(Deployment, nsBatch, customServiceDeploymentName) + assert.NoError(t, err) + err = helper.ValidatePodsAnnotations(nsBatch, + getAnnotations(instrumentation.TypeDotNet, instrumentation.TypeNodeJS), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython)) + assert.NoError(t, err) //FAILING: pod is annotated .net & nodejs + err = helper.ValidateWorkloadAnnotations(Deployment, nsBatch, customServiceDeploymentName, + getAnnotations(instrumentation.TypeNodeJS, instrumentation.TypeDotNet), + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython)) + assert.NoError(t, err) +} + +// Permutation 22 [LOW]: Enable monitoring AND No AutoRestart with granular configuration including languages, exclusion and custom selector +func TestPermutation22_SelectiveMonitoringWithCustomSelector(t *testing.T) { + helper := NewTestHelper(t) + + nsSecurity := "high-security" + nsLegacy := "legacy" + namespace := helper.Initialize("test-namespace") + // Create deployments + err := helper.CreateNamespaceAndApplyResources(namespace, []string{sampleDeploymentYaml, sampleDeploymentServiceYaml, customerServiceYaml}, false) + assert.NoError(t, err) + // more deployments to different NSes + err = helper.CreateNamespaceAndApplyResources(nsSecurity, []string{customerServiceYaml}, false) + assert.NoError(t, err) + err = helper.CreateNamespaceAndApplyResources(nsLegacy, []string{customerServiceYaml}, false) + assert.NoError(t, err) + + // Update operator config + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + Languages: instrumentation.NewTypeSet(instrumentation.TypeJava, instrumentation.TypePython), + RestartPods: true, + CustomSelector: auto.AnnotationConfig{ + DotNet: auto.AnnotationResources{ + Deployments: []string{nsLegacy + "/" + customServiceDeploymentName}, + }, + }, + Exclude: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{nsSecurity}, + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + }, + }, + }) + + //postcheck + // java and python + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython), + getAnnotations(instrumentation.TypeNodeJS, instrumentation.TypeDotNet)) + assert.NoError(t, err) + // include dotnet by custom selector + err = helper.ValidateWorkloadAnnotations(Deployment, nsLegacy, customServiceDeploymentName, + getAnnotations(instrumentation.TypeJava, instrumentation.TypePython, instrumentation.TypeDotNet), + getAnnotations(instrumentation.TypeNodeJS)) + assert.NoError(t, err) + // exclude java by ns + err = helper.ValidateWorkloadAnnotations(Deployment, nsSecurity, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython), + getAnnotations(instrumentation.TypeJava, instrumentation.TypeNodeJS, instrumentation.TypeDotNet)) + assert.NoError(t, err) + // exclude java by workload + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython), + getAnnotations(instrumentation.TypeJava, instrumentation.TypeNodeJS, instrumentation.TypeDotNet)) + assert.NoError(t, err) + + // Update operator config by dropping languages + helper.UpdateMonitorConfig(&auto.MonitorConfig{ + MonitorAllServices: true, + RestartPods: true, + CustomSelector: auto.AnnotationConfig{ + DotNet: auto.AnnotationResources{ + Deployments: []string{nsLegacy + "/" + customServiceDeploymentName}, + }, + }, + Exclude: auto.AnnotationConfig{ + Java: auto.AnnotationResources{ + Namespaces: []string{nsSecurity}, + Deployments: []string{namespace + "/" + customServiceDeploymentName}, + }, + }, + }) + + //postcheck + // all + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, deploymentName, allAnnotations, none) + assert.NoError(t, err) + err = helper.ValidateWorkloadAnnotations(Deployment, nsLegacy, customServiceDeploymentName, allAnnotations, none) + assert.NoError(t, err) + // exclude java by ns + err = helper.ValidateWorkloadAnnotations(Deployment, nsSecurity, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython, instrumentation.TypeNodeJS, instrumentation.TypeDotNet), + getAnnotations(instrumentation.TypeJava)) + assert.NoError(t, err) + // exclude java by workload + err = helper.ValidateWorkloadAnnotations(Deployment, namespace, customServiceDeploymentName, + getAnnotations(instrumentation.TypePython, instrumentation.TypeNodeJS, instrumentation.TypeDotNet), + getAnnotations(instrumentation.TypeJava)) + assert.NoError(t, err) +} diff --git a/integration-tests/manifests/conflicting-deployment.yaml b/integration-tests/manifests/conflicting-deployment.yaml new file mode 100644 index 000000000..ece71b497 --- /dev/null +++ b/integration-tests/manifests/conflicting-deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: conflicting-deployment + labels: + app: conflicting-app +spec: + replicas: 1 + selector: + matchLabels: + app: conflicting-app + template: + metadata: + labels: + app: conflicting-app + spec: + containers: + - name: conflicting-container + image: nginx:latest + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: conflicting-service +spec: + selector: + app: conflicting-app + ports: + - port: 80 + targetPort: 80 diff --git a/integration-tests/manifests/customer-service.yaml b/integration-tests/manifests/customer-service.yaml new file mode 100644 index 000000000..d4af762b1 --- /dev/null +++ b/integration-tests/manifests/customer-service.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: customer-service + labels: + app: customer-service +spec: + replicas: 1 + selector: + matchLabels: + app: customer-service + template: + metadata: + labels: + app: customer-service + spec: + containers: + - name: customer-service-container + image: nginx:latest + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: customer-service +spec: + selector: + app: customer-service + ports: + - port: 80 + targetPort: 80 diff --git a/integration-tests/manifests/frontend-app.yaml b/integration-tests/manifests/frontend-app.yaml new file mode 100644 index 000000000..c833a1358 --- /dev/null +++ b/integration-tests/manifests/frontend-app.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-app + labels: + app: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend-container + image: nginx:latest + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend-service +spec: + selector: + app: frontend + ports: + - port: 80 + targetPort: 80 diff --git a/integration-tests/manifests/sample-deployment-service.yaml b/integration-tests/manifests/sample-deployment-service.yaml new file mode 100644 index 000000000..2af8e448b --- /dev/null +++ b/integration-tests/manifests/sample-deployment-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: sample-deployment-service +spec: + selector: + app: nginx-app + ports: + - protocol: TCP + port: 80 + targetPort: 80 \ No newline at end of file diff --git a/integration-tests/manifests/sample-deployment-without-service.yaml b/integration-tests/manifests/sample-deployment-without-service.yaml new file mode 100644 index 000000000..e58e35eb1 --- /dev/null +++ b/integration-tests/manifests/sample-deployment-without-service.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sample-deployment-without-service + labels: + app: sample-app-without-service +spec: + replicas: 1 + selector: + matchLabels: + app: sample-app-without-service + template: + metadata: + labels: + app: sample-app-without-service + spec: + containers: + - name: sample-container + image: nginx:latest + ports: + - containerPort: 80