github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/containerizedworkload/containerizedworkload_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 containerizedworkload
     5  
     6  import (
     7  	"context"
     8  	"os"
     9  	"strings"
    10  	"testing"
    11  
    12  	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
    13  	"github.com/crossplane/crossplane-runtime/pkg/meta"
    14  	oamcore "github.com/crossplane/oam-kubernetes-runtime/apis/core"
    15  	oamv1 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
    16  	"github.com/crossplane/oam-kubernetes-runtime/pkg/oam"
    17  	"github.com/golang/mock/gomock"
    18  	asserts "github.com/stretchr/testify/assert"
    19  	"github.com/verrazzano/verrazzano/application-operator/mocks"
    20  	vzconst "github.com/verrazzano/verrazzano/pkg/constants"
    21  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    22  	"go.uber.org/zap"
    23  	appsv1 "k8s.io/api/apps/v1"
    24  	corev1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	ctrl "sigs.k8s.io/controller-runtime"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  	fakes "sigs.k8s.io/controller-runtime/pkg/client/fake"
    32  	"sigs.k8s.io/yaml"
    33  )
    34  
    35  const (
    36  	testRestartVersion = "new-restart"
    37  	testNamespace      = "test-namespace"
    38  
    39  	appConfigName = "test-appconf"
    40  	componentName = "test-component"
    41  )
    42  
    43  // newScheme creates a new scheme that includes this package's object to use for testing
    44  func newScheme() *runtime.Scheme {
    45  	scheme := runtime.NewScheme()
    46  	_ = oamcore.AddToScheme(scheme)
    47  	return scheme
    48  }
    49  
    50  // newReconciler creates a new reconciler for testing
    51  func newReconciler(c client.Client) Reconciler {
    52  	return Reconciler{
    53  		Client: c,
    54  		Log:    zap.S().With("test"),
    55  		Scheme: newScheme(),
    56  	}
    57  }
    58  
    59  // newRequest creates a new reconciler request for testing
    60  func newRequest(namespace string, name string) ctrl.Request {
    61  	return ctrl.Request{
    62  		NamespacedName: types.NamespacedName{
    63  			Namespace: namespace,
    64  			Name:      name,
    65  		},
    66  	}
    67  }
    68  
    69  // TestReconcileRestart tests reconciling a ContainerizedWorkload when the restart-version specified in the annotations.
    70  // This should result in restart-version written to the Deployment.
    71  // GIVEN a ContainerizedWorkload resource
    72  // WHEN the controller Reconcile function is called and the restart-version is specified
    73  // THEN the restart-version written
    74  func TestReconcileRestart(t *testing.T) {
    75  	assert := asserts.New(t)
    76  	var mocker = gomock.NewController(t)
    77  	var cli = mocks.NewMockClient(mocker)
    78  
    79  	labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: appConfigName}
    80  	annotations := map[string]string{vzconst.RestartVersionAnnotation: testRestartVersion}
    81  
    82  	// expect a call to fetch the ContainerizedWorkload
    83  	params := map[string]string{
    84  		"##OAM_APP_NAME##":         "test-oam-app-name",
    85  		"##OAM_COMP_NAME##":        "test-oam-comp-name",
    86  		"##TRAIT_NAME##":           "test-trait-name",
    87  		"##TRAIT_NAMESPACE##":      "test-namespace",
    88  		"##WORKLOAD_APIVER##":      "core.oam.dev/v1alpha2",
    89  		"##WORKLOAD_KIND##":        "ContainerizedWorkload",
    90  		"##WORKLOAD_NAME##":        "test-workload-name",
    91  		"##PROMETHEUS_NAME##":      "vmi-system-prometheus-0",
    92  		"##PROMETHEUS_NAMESPACE##": "verrazzano-system",
    93  		"##DEPLOYMENT_NAMESPACE##": "test-namespace",
    94  		"##DEPLOYMENT_NAME##":      "test-workload-name",
    95  	}
    96  	cli.EXPECT().
    97  		Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-verrazzano-containerized-workload"}, gomock.Not(gomock.Nil()), gomock.Any()).
    98  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *oamv1.ContainerizedWorkload, opts ...client.GetOption) error {
    99  			assert.NoError(updateObjectFromYAMLTemplate(workload, "testdata/templates/containerized_workload_deployment.yaml", params))
   100  			workload.ObjectMeta.Labels = labels
   101  			workload.ObjectMeta.Annotations = annotations
   102  			return nil
   103  		}).Times(1)
   104  	// expect a call to list the deployment
   105  	cli.EXPECT().
   106  		List(gomock.Any(), gomock.Any(), gomock.Any()).
   107  		DoAndReturn(func(ctx context.Context, list *appsv1.DeploymentList, opts ...client.ListOption) error {
   108  			list.Items = []appsv1.Deployment{*getTestDeployment("")}
   109  			return nil
   110  		})
   111  	// expect a call to fetch the deployment
   112  	cli.EXPECT().
   113  		Get(gomock.Any(), gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()).
   114  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opts ...client.GetOption) error {
   115  			annotateRestartVersion(deployment, "")
   116  			return nil
   117  		})
   118  	// expect a call to update the deployment
   119  	cli.EXPECT().
   120  		Update(gomock.Any(), gomock.AssignableToTypeOf(&appsv1.Deployment{}), gomock.Any()).
   121  		DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, opts ...client.UpdateOption) error {
   122  			assert.Equal(testRestartVersion, deploy.Spec.Template.ObjectMeta.Annotations[vzconst.RestartVersionAnnotation])
   123  			return nil
   124  		})
   125  
   126  	// create a request and reconcile it
   127  	request := newRequest(testNamespace, "test-verrazzano-containerized-workload")
   128  	reconciler := newReconciler(cli)
   129  	result, err := reconciler.Reconcile(context.TODO(), request)
   130  
   131  	mocker.Finish()
   132  	assert.NoError(err)
   133  	assert.Equal(false, result.Requeue)
   134  }
   135  
   136  // TestReconcileKubeSystem tests to make sure we do not reconcile
   137  // Any resource that belong to the kube-system namespace
   138  func TestReconcileKubeSystem(t *testing.T) {
   139  	assert := asserts.New(t)
   140  	mocker := gomock.NewController(t)
   141  	cli := mocks.NewMockClient(mocker)
   142  
   143  	// create a request and reconcile it
   144  	request := newRequest(vzconst.KubeSystem, "test-verrazzano-containerized-workload")
   145  	reconciler := newReconciler(cli)
   146  	result, err := reconciler.Reconcile(context.TODO(), request)
   147  
   148  	// Validate the results
   149  	mocker.Finish()
   150  	assert.NoError(err)
   151  	assert.True(result.IsZero())
   152  }
   153  
   154  // TestGetWorkloadService tests to make sure the workload service is being picked up
   155  func TestGetWorkloadService(t *testing.T) {
   156  	a := asserts.New(t)
   157  	log := vzlog.DefaultLogger()
   158  
   159  	// GIVEN a ContainerizedWorkload resource
   160  	// WHEN the status is not populated with a service
   161  	// THEN no service is returned
   162  	emptyWorkload := oamv1.ContainerizedWorkload{}
   163  	cli := fakes.NewClientBuilder().Build()
   164  	reconciler := newReconciler(cli)
   165  	svc, err := reconciler.getWorkloadService(context.TODO(), emptyWorkload, log)
   166  	a.Nil(svc)
   167  	a.NoError(err)
   168  
   169  	// GIVEN a ContainerizedWorkload resource
   170  	// WHEN the status is populated with a service that doesn't exist
   171  	// THEN an error is returned
   172  	service := corev1.Service{
   173  		TypeMeta: metav1.TypeMeta{
   174  			Kind:       "Service",
   175  			APIVersion: "v1",
   176  		},
   177  		ObjectMeta: metav1.ObjectMeta{
   178  			Name:   "test-service",
   179  			UID:    "test-uid",
   180  			Labels: map[string]string{},
   181  		},
   182  	}
   183  	statusWorkload := oamv1.ContainerizedWorkload{
   184  		Status: oamv1.ContainerizedWorkloadStatus{
   185  			Resources: []commonv1.TypedReference{*meta.TypedReferenceTo(&service, service.GroupVersionKind())},
   186  		},
   187  	}
   188  	cli = fakes.NewClientBuilder().Build()
   189  	reconciler = newReconciler(cli)
   190  	svc, err = reconciler.getWorkloadService(context.TODO(), statusWorkload, log)
   191  	a.Nil(svc)
   192  	a.Error(err)
   193  
   194  	// GIVEN a ContainerizedWorkload resource
   195  	// WHEN the status is populated with a service that exists
   196  	// THEN the service is returned
   197  	cli = fakes.NewClientBuilder().WithObjects(&service).Build()
   198  	reconciler = newReconciler(cli)
   199  	svc, err = reconciler.getWorkloadService(context.TODO(), statusWorkload, log)
   200  	a.Equal(svc.Name, service.Name)
   201  	a.Equal(svc.UID, service.UID)
   202  	a.NoError(err)
   203  
   204  }
   205  
   206  // TestUpdateServiceLabels tests the update to the service in the containerized workload status
   207  func TestUpdateServiceLabels(t *testing.T) {
   208  	a := asserts.New(t)
   209  
   210  	// GIVEN a ContainerizedWorkload resource
   211  	// WHEN the status is empty
   212  	// THEN no error is returned
   213  	statusWorkload := oamv1.ContainerizedWorkload{}
   214  	cli := fakes.NewClientBuilder().Build()
   215  	reconciler := newReconciler(cli)
   216  	err := reconciler.updateServiceLabels(context.TODO(), statusWorkload, vzlog.DefaultLogger())
   217  	a.NoError(err)
   218  }
   219  
   220  // updateObjectFromYAMLTemplate updates an object from a populated YAML template file.
   221  // uns - The unstructured to update
   222  // template - The template file
   223  // params - The param maps to merge into the template
   224  func updateObjectFromYAMLTemplate(obj interface{}, template string, params ...map[string]string) error {
   225  	uns := unstructured.Unstructured{}
   226  	err := updateUnstructuredFromYAMLTemplate(&uns, template, params...)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	err = runtime.DefaultUnstructuredConverter.FromUnstructured(uns.Object, obj)
   231  	if err != nil {
   232  		return err
   233  	}
   234  	return nil
   235  }
   236  
   237  // updateUnstructuredFromYAMLTemplate updates an unstructured from a populated YAML template file.
   238  // uns - The unstructured to update
   239  // template - The template file
   240  // params - The param maps to merge into the template
   241  func updateUnstructuredFromYAMLTemplate(uns *unstructured.Unstructured, template string, params ...map[string]string) error {
   242  	str, err := readTemplate(template, params...)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	bytes, err := yaml.YAMLToJSON([]byte(str))
   247  	if err != nil {
   248  		return err
   249  	}
   250  	_, _, err = unstructured.UnstructuredJSONScheme.Decode(bytes, nil, uns)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	return nil
   255  }
   256  
   257  // readTemplate reads a string template from a file and replaces values in the template from param maps
   258  // template - The filename of a template
   259  // params - a vararg of param maps
   260  func readTemplate(template string, params ...map[string]string) (string, error) {
   261  	bytes, err := os.ReadFile("../../" + template)
   262  	if err != nil {
   263  		bytes, err = os.ReadFile("../" + template)
   264  		if err != nil {
   265  			bytes, err = os.ReadFile(template)
   266  			if err != nil {
   267  				return "", err
   268  			}
   269  		}
   270  	}
   271  	content := string(bytes)
   272  	for _, p := range params {
   273  		for k, v := range p {
   274  			content = strings.ReplaceAll(content, k, v)
   275  		}
   276  	}
   277  	return content, nil
   278  }
   279  
   280  func getTestDeployment(restartVersion string) *appsv1.Deployment {
   281  	deployment := &appsv1.Deployment{}
   282  	annotateRestartVersion(deployment, restartVersion)
   283  	return deployment
   284  }
   285  
   286  func annotateRestartVersion(deployment *appsv1.Deployment, restartVersion string) {
   287  	deployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
   288  	deployment.Spec.Template.ObjectMeta.Annotations[vzconst.RestartVersionAnnotation] = restartVersion
   289  }