github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/loggingtrait/loggingtrait_controller_test.go (about)

     1  // Copyright (c) 2021, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package loggingtrait
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"testing"
    11  	"time"
    12  
    13  	vzapi "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1"
    14  	"github.com/verrazzano/verrazzano/application-operator/mocks"
    15  	vzconst "github.com/verrazzano/verrazzano/pkg/constants"
    16  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    17  
    18  	oamrt "github.com/crossplane/crossplane-runtime/apis/common/v1"
    19  	oamcore "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
    20  	"github.com/go-logr/logr"
    21  	"github.com/golang/mock/gomock"
    22  	asserts "github.com/stretchr/testify/assert"
    23  	"go.uber.org/zap"
    24  	k8sapps "k8s.io/api/apps/v1"
    25  	corev1 "k8s.io/api/core/v1"
    26  	"k8s.io/apimachinery/pkg/api/errors"
    27  	k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	ctrl "sigs.k8s.io/controller-runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	// +kubebuilder:scaffold:imports
    34  )
    35  
    36  var (
    37  	namespaceName               = "test-namespace"
    38  	workloadName                = "test-workload-name"
    39  	workloadUID                 = "test-workload-uid"
    40  	traitName                   = "test-trait-name"
    41  	deploymentName              = "test-deployment-name"
    42  	workloadDefinitionNamespace = "containerizedworkloads.core.oam.dev"
    43  	serverErr                   = "server error"
    44  )
    45  
    46  func TestReconcilerSetupWithManager(t *testing.T) {
    47  	assert := asserts.New(t)
    48  
    49  	var mocker *gomock.Controller
    50  	var mgr *mocks.MockManager
    51  	var cli *mocks.MockClient
    52  	var scheme *runtime.Scheme
    53  
    54  	var reconciler LoggingTraitReconciler
    55  	var err error
    56  
    57  	mocker = gomock.NewController(t)
    58  	mgr = mocks.NewMockManager(mocker)
    59  	cli = mocks.NewMockClient(mocker)
    60  	scheme = runtime.NewScheme()
    61  	_ = vzapi.AddToScheme(scheme)
    62  	reconciler = LoggingTraitReconciler{Client: cli, Scheme: scheme}
    63  	mgr.EXPECT().GetControllerOptions().AnyTimes()
    64  	mgr.EXPECT().GetScheme().Return(scheme)
    65  	mgr.EXPECT().GetLogger().Return(logr.Discard())
    66  	mgr.EXPECT().SetFields(gomock.Any()).Return(nil).AnyTimes()
    67  	mgr.EXPECT().Add(gomock.Any()).Return(nil).AnyTimes()
    68  	err = reconciler.SetupWithManager(mgr)
    69  	mocker.Finish()
    70  	assert.NoError(err)
    71  }
    72  
    73  // TestLoggingTraitCreatedForContainerizedWorkload tests the creation of a logging trait related to a containerized workload.
    74  // GIVEN a logging trait that has been created
    75  // AND the logging trait is related to a containerized workload
    76  // WHEN the logging trait Reconcile method is invoked
    77  // THEN verify that logging trait finalizer is added
    78  // AND verify that pod annotations are updated
    79  // AND verify that the scraper configmap is updated
    80  // AND verify that the scraper pod is restarted
    81  func TestLoggingTraitCreatedForContainerizedWorkload(t *testing.T) {
    82  	assert := asserts.New(t)
    83  	mocker := gomock.NewController(t)
    84  	mock := mocks.NewMockClient(mocker)
    85  	mockStatus := mocks.NewMockStatusWriter(mocker)
    86  
    87  	testDeployment := newDeployment(deploymentName, namespaceName, workloadName, workloadUID)
    88  
    89  	// Expect a call to get the logging trait
    90  	mock.EXPECT().
    91  		Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: traitName}), gomock.Not(gomock.Nil()), gomock.Any()).
    92  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, trait *vzapi.LoggingTrait, opt ...client.GetOption) error {
    93  			trait.SetWorkloadReference(oamrt.TypedReference{
    94  				APIVersion: oamcore.SchemeGroupVersion.Identifier(),
    95  				Kind:       oamcore.ContainerizedWorkloadKind,
    96  				Name:       workloadName,
    97  				UID:        types.UID(workloadUID),
    98  			})
    99  			trait.SetNamespace(namespaceName)
   100  			return nil
   101  		})
   102  	// Expect a call to get the workload
   103  	mock.EXPECT().
   104  		Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()).
   105  		DoAndReturn(func(ctx context.Context, key client.ObjectKey, workload *unstructured.Unstructured, opt ...client.GetOption) error {
   106  			return nil
   107  		})
   108  	// Expect a call to get the workload definition
   109  	mock.EXPECT().
   110  		Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()).
   111  		DoAndReturn(func(ctx context.Context, key client.ObjectKey, workloadDef *oamcore.WorkloadDefinition, opt ...client.GetOption) error {
   112  			workloadDef.Spec.ChildResourceKinds = []oamcore.ChildResourceKind{
   113  				{
   114  					APIVersion: k8sapps.SchemeGroupVersion.Identifier(),
   115  					Kind:       "Deployment",
   116  				},
   117  			}
   118  			return nil
   119  		})
   120  	// Expect to list config map
   121  	options := []client.ListOption{client.InNamespace(namespaceName), client.MatchingFields{"metadata.name": "logging-stdout-test-deployment-name-deployment"}}
   122  	mock.EXPECT().
   123  		List(gomock.Any(), gomock.Not(gomock.Nil()), options).
   124  		DoAndReturn(func(ctx context.Context, list *unstructured.UnstructuredList, opts ...client.ListOption) error {
   125  			return nil
   126  		})
   127  	// Expect to create a config map
   128  	mock.EXPECT().
   129  		Create(gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()).
   130  		DoAndReturn(func(ctx context.Context, configMap *corev1.ConfigMap, opts ...client.CreateOption) error {
   131  			return nil
   132  		})
   133  	// Expect a call to get the deployment
   134  	mock.EXPECT().
   135  		Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: deploymentName}), gomock.Not(gomock.Nil()), gomock.Any()).
   136  		DoAndReturn(func(ctx context.Context, key client.ObjectKey, workload *unstructured.Unstructured, opt ...client.GetOption) error {
   137  			return nil
   138  		})
   139  	// Expect a call to list the child Deployment resources of the containerized workload definition
   140  	mock.EXPECT().
   141  		List(gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()).
   142  		DoAndReturn(func(ctx context.Context, list *unstructured.UnstructuredList, opts ...client.ListOption) error {
   143  			assert.Equal("Deployment", list.GetKind())
   144  			return appendAsUnstructured(list, testDeployment)
   145  		})
   146  	// Expect a call to get the status writer
   147  	mock.EXPECT().Status().Return(mockStatus).AnyTimes()
   148  
   149  	// Create and make the request
   150  	request := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: namespaceName, Name: "test-trait-name"}}
   151  
   152  	reconciler := newLoggingTraitReconciler(mock, t)
   153  	result, err := reconciler.Reconcile(context.TODO(), request)
   154  
   155  	// Validate the results
   156  	mocker.Finish()
   157  	assert.NoError(err)
   158  	assert.Equal(time.Duration(0), result.RequeueAfter)
   159  }
   160  
   161  // TestDeleteLoggingTraitFromContainerizedWorkload tests the deletion of a logging trait related to a containerized workload.
   162  // GIVEN a logging trait
   163  // AND the logging trait is related to a containerized workload
   164  // WHEN the logging trait reconcileTraitDelete method is invoked
   165  // THEN verify that the logging trait has been deleted
   166  func TestDeleteLoggingTraitFromContainerizedWorkload(t *testing.T) {
   167  	assert := asserts.New(t)
   168  	mocker := gomock.NewController(t)
   169  	mock := mocks.NewMockClient(mocker)
   170  
   171  	testDeployment := newDeployment(deploymentName, namespaceName, workloadName, workloadUID)
   172  
   173  	// Create trait for deletion
   174  	trait := vzapi.LoggingTrait{
   175  		TypeMeta: k8smeta.TypeMeta{
   176  			Kind: vzapi.LoggingTraitKind,
   177  		},
   178  		ObjectMeta: k8smeta.ObjectMeta{
   179  			Name:      traitName,
   180  			Namespace: namespaceName,
   181  		},
   182  		Spec: vzapi.LoggingTraitSpec{
   183  			WorkloadReference: oamrt.TypedReference{
   184  				APIVersion: oamcore.SchemeGroupVersion.Identifier(),
   185  				Kind:       oamcore.ContainerizedWorkloadKind,
   186  				Name:       workloadName,
   187  				UID:        types.UID(workloadUID),
   188  			},
   189  		},
   190  		Status: vzapi.LoggingTraitStatus{},
   191  	}
   192  
   193  	// Expect a call to get the workload
   194  	mock.EXPECT().
   195  		Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()).
   196  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *unstructured.Unstructured, opt ...client.GetOption) error {
   197  			workload.SetNamespace(namespaceName)
   198  			return nil
   199  		})
   200  	// Expect a call to get the workload definition
   201  	mock.EXPECT().
   202  		Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()).
   203  		DoAndReturn(func(ctx context.Context, key client.ObjectKey, workloadDef *oamcore.WorkloadDefinition, opt ...client.GetOption) error {
   204  			workloadDef.Spec.ChildResourceKinds = []oamcore.ChildResourceKind{
   205  				{
   206  					APIVersion: k8sapps.SchemeGroupVersion.Identifier(),
   207  					Kind:       "Deployment",
   208  				},
   209  			}
   210  			return nil
   211  		})
   212  	// Expect to list deployment
   213  	options := []client.ListOption{client.InNamespace(namespaceName)}
   214  	mock.EXPECT().
   215  		List(gomock.Any(), gomock.Not(gomock.Nil()), options).
   216  		DoAndReturn(func(ctx context.Context, deployment *unstructured.UnstructuredList, opts ...client.ListOption) error {
   217  			unstructuredDeployment, err := convertToUnstructured(testDeployment)
   218  			if err != nil {
   219  				t.Fatalf("Could not create unstructured Deployment")
   220  			}
   221  			deployment.Items = []unstructured.Unstructured{unstructuredDeployment}
   222  			return nil
   223  		})
   224  	// Expect a call to get the deployment
   225  	mock.EXPECT().
   226  		Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: deploymentName}), gomock.Not(gomock.Nil()), gomock.Any()).
   227  		DoAndReturn(func(ctx context.Context, key client.ObjectKey, workload *unstructured.Unstructured, opt ...client.GetOption) error {
   228  			return nil
   229  		})
   230  	// Expect to list config map
   231  	mock.EXPECT().
   232  		List(gomock.Any(), gomock.Not(gomock.Nil()), client.InNamespace(namespaceName), client.MatchingFields{"metadata.name": "logging-stdout-test-deployment-name-deployment"}).
   233  		DoAndReturn(func(ctx context.Context, list *unstructured.UnstructuredList, options ...client.ListOption) error {
   234  			return nil
   235  		})
   236  
   237  	reconciler := newLoggingTraitReconciler(mock, t)
   238  	result, err := reconciler.reconcileTraitDelete(context.TODO(), vzlog.DefaultLogger(), &trait)
   239  
   240  	// Validate the results
   241  	mocker.Finish()
   242  	assert.NoError(err)
   243  	assert.Equal(time.Duration(0), result.RequeueAfter)
   244  }
   245  
   246  // Test_fetchTrait tests the fetchTrait function of the LoggingTraitReconciler
   247  // GIVEN a call to fetchTrait method of the LoggingTrait Reconciler
   248  // WHEN there is some error during retrieving the trait
   249  // THEN expect the reconciler to requeue and return no error
   250  func TestFetchTraitError(t *testing.T) {
   251  	assert := asserts.New(t)
   252  	mocker := gomock.NewController(t)
   253  	mock := mocks.NewMockClient(mocker)
   254  
   255  	request := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: namespaceName, Name: traitName}}
   256  
   257  	mock.EXPECT().
   258  		Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: traitName}), gomock.Not(gomock.Nil()), gomock.Any()).
   259  		Return(
   260  			fmt.Errorf(serverErr),
   261  		)
   262  
   263  	reconciler := newLoggingTraitReconciler(mock, t)
   264  	result, err := reconciler.Reconcile(context.TODO(), request)
   265  	assert.Nil(err)
   266  	assert.NotNil(result)
   267  	assert.True(result.Requeue)
   268  
   269  	mock = mocks.NewMockClient(mocker)
   270  	mock.EXPECT().
   271  		Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: traitName}), gomock.Not(gomock.Nil()), gomock.Any()).
   272  		Return(
   273  			&errors.StatusError{ErrStatus: k8smeta.Status{Code: 404}},
   274  		)
   275  
   276  	reconciler = newLoggingTraitReconciler(mock, t)
   277  	result, err = reconciler.Reconcile(context.TODO(), request)
   278  	assert.Nil(err)
   279  	assert.NotNil(result)
   280  	assert.True(result.Requeue)
   281  }
   282  
   283  // TestCreateOrUpdateLoggingTraitNoWorkloadChild tests the creation/update/deletion of LoggingTrait when
   284  // no child resources for workload exists
   285  // GIVEN a LoggingTrait with workload reference
   286  // WHEN there is no child resource of the workload
   287  // THEN fall back to the original workload and complete the reconciliation
   288  func TestReconcileTraitNoWorkloadChild(t *testing.T) {
   289  	assert := asserts.New(t)
   290  	mocker := gomock.NewController(t)
   291  	mock := mocks.NewMockClient(mocker)
   292  
   293  	trait := &vzapi.LoggingTrait{
   294  		TypeMeta: k8smeta.TypeMeta{
   295  			Kind: vzapi.LoggingTraitKind,
   296  		},
   297  		ObjectMeta: k8smeta.ObjectMeta{
   298  			Name:      traitName,
   299  			Namespace: namespaceName,
   300  		},
   301  		Spec: vzapi.LoggingTraitSpec{
   302  			WorkloadReference: oamrt.TypedReference{
   303  				APIVersion: oamcore.SchemeGroupVersion.Identifier(),
   304  				Kind:       oamcore.ContainerizedWorkloadKind,
   305  				Name:       workloadName,
   306  				UID:        types.UID(workloadUID),
   307  			}},
   308  	}
   309  
   310  	mock.EXPECT().
   311  		Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()).
   312  		DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured, opt ...client.GetOption) error {
   313  			return nil
   314  		})
   315  
   316  	mock.EXPECT().
   317  		Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()).
   318  		Return(fmt.Errorf(serverErr))
   319  
   320  	reconciler := newLoggingTraitReconciler(mock, t)
   321  	result, supported, err := reconciler.reconcileTraitCreateOrUpdate(context.TODO(), vzlog.DefaultLogger(), trait)
   322  	assert.NoError(err)
   323  	assert.NotNil(result)
   324  	assert.True(supported)
   325  
   326  	mock = mocks.NewMockClient(mocker)
   327  	mock.EXPECT().
   328  		Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()).
   329  		DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured, opt ...client.GetOption) error {
   330  			return nil
   331  		})
   332  
   333  	mock.EXPECT().
   334  		Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()).
   335  		Return(fmt.Errorf(serverErr))
   336  
   337  	reconciler = newLoggingTraitReconciler(mock, t)
   338  	result, err = reconciler.reconcileTraitDelete(context.TODO(), vzlog.DefaultLogger(), trait)
   339  	assert.NoError(err)
   340  	assert.NotNil(result)
   341  }
   342  
   343  // convertToUnstructured converts an object to an Unstructured version
   344  // object - The object to convert to Unstructured
   345  func convertToUnstructured(object interface{}) (unstructured.Unstructured, error) {
   346  	jbytes, err := json.Marshal(object)
   347  	if err != nil {
   348  		return unstructured.Unstructured{}, err
   349  	}
   350  	var u map[string]interface{}
   351  	_ = json.Unmarshal(jbytes, &u)
   352  	return unstructured.Unstructured{Object: u}, nil
   353  }
   354  
   355  // appendAsUnstructured appends an object to the list after converting it to an Unstructured
   356  // list - The list to append to.
   357  // object - The object to convert to Unstructured and append to the list
   358  func appendAsUnstructured(list *unstructured.UnstructuredList, object interface{}) error {
   359  	u, err := convertToUnstructured(object)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	list.Items = append(list.Items, u)
   364  	return nil
   365  }
   366  
   367  // newLoggingTraitReconciler creates a new reconciler for testing
   368  // cli - The Kerberos client to inject into the reconciler
   369  func newLoggingTraitReconciler(cli client.Client, t *testing.T) LoggingTraitReconciler {
   370  	scheme := runtime.NewScheme()
   371  	vzapi.AddToScheme(scheme)
   372  	reconciler := LoggingTraitReconciler{
   373  		Client: cli,
   374  		Log:    zap.S(),
   375  		Scheme: scheme,
   376  	}
   377  	return reconciler
   378  }
   379  
   380  func newDeployment(deploymentName string, namespaceName string, workloadName string, workloadUID string) k8sapps.Deployment {
   381  	return k8sapps.Deployment{
   382  		TypeMeta: k8smeta.TypeMeta{
   383  			APIVersion: k8sapps.SchemeGroupVersion.Identifier(),
   384  			Kind:       "Deployment",
   385  		},
   386  		ObjectMeta: k8smeta.ObjectMeta{
   387  			Name:              deploymentName,
   388  			Namespace:         namespaceName,
   389  			CreationTimestamp: k8smeta.Now(),
   390  			OwnerReferences: []k8smeta.OwnerReference{
   391  				{
   392  					APIVersion: oamcore.SchemeGroupVersion.Identifier(),
   393  					Kind:       oamcore.ContainerizedWorkloadKind,
   394  					Name:       workloadName,
   395  					UID:        types.UID(workloadUID),
   396  				},
   397  			},
   398  		},
   399  		Spec: k8sapps.DeploymentSpec{
   400  			Template: corev1.PodTemplateSpec{
   401  				Spec: corev1.PodSpec{
   402  					Containers: []corev1.Container{
   403  						{
   404  							Name: "test-container",
   405  						},
   406  					},
   407  				},
   408  			},
   409  		},
   410  	}
   411  }
   412  
   413  // TestReconcileKubeSystem tests to make sure we do not reconcile
   414  // Any resource that belong to the kube-system namespace
   415  func TestReconcileKubeSystem(t *testing.T) {
   416  	assert := asserts.New(t)
   417  	mocker := gomock.NewController(t)
   418  	mock := mocks.NewMockClient(mocker)
   419  
   420  	// create a request and reconcile it
   421  	request := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: vzconst.KubeSystem, Name: traitName}}
   422  	reconciler := newLoggingTraitReconciler(mock, t)
   423  	result, err := reconciler.Reconcile(context.TODO(), request)
   424  
   425  	// Validate the results
   426  	mocker.Finish()
   427  	assert.Nil(err)
   428  	assert.True(result.IsZero())
   429  }