diff --git a/controllers/nstemplatetier/nstemplatetier_controller.go b/controllers/nstemplatetier/nstemplatetier_controller.go index 2aa4754c8..fe27f36d2 100644 --- a/controllers/nstemplatetier/nstemplatetier_controller.go +++ b/controllers/nstemplatetier/nstemplatetier_controller.go @@ -2,14 +2,17 @@ package nstemplatetier import ( "context" + "fmt" + "time" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/host-operator/controllers/toolchainconfig" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/log" - errs "github.com/pkg/errors" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -49,13 +52,173 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl. } // Error reading the object - requeue the request. logger.Error(err, "unable to get the current NSTemplateTier") - return reconcile.Result{}, errs.Wrap(err, "unable to get the current NSTemplateTier") + return reconcile.Result{}, fmt.Errorf("unable to get the current NSTemplateTier: %w", err) } _, err := toolchainconfig.GetToolchainConfig(r.Client) if err != nil { - return reconcile.Result{}, errs.Wrapf(err, "unable to get ToolchainConfig") + return reconcile.Result{}, fmt.Errorf("unable to get ToolchainConfig: %w", err) + } + + // check if the `status.revisions` field is up-to-date and create a TTR for each TierTemplate + if created, err := r.ensureRevision(ctx, tier); err != nil { + // todo add/update ready condition false in the NSTemplateTier when something fails + return reconcile.Result{}, fmt.Errorf("unable to create new TierTemplateRevision after NSTemplateTier changed: %w", err) + } else if created { + logger.Info("Requeue after creating a new TTR") + return reconcile.Result{RequeueAfter: time.Second}, nil } return reconcile.Result{}, nil } + +// ensureRevision ensures that there is a TierTemplateRevision CR for each of the TierTemplate. +// returns `true` if a new TierTemplateRevision CR was created, `err` if something wrong happened +func (r *Reconciler) ensureRevision(ctx context.Context, nsTmplTier *toolchainv1alpha1.NSTemplateTier) (bool, error) { + logger := log.FromContext(ctx) + refs := getNSTemplateTierRefs(nsTmplTier) + + // init revisions + if nsTmplTier.Status.Revisions == nil { + nsTmplTier.Status.Revisions = map[string]string{} + } + // check for TierTemplates and TierTemplateRevisions associated with the NSTemplateTier + ttrCreated := false + for _, tierTemplateRef := range refs { + // get the TierTemplate + var tierTemplate toolchainv1alpha1.TierTemplate + if err := r.Client.Get(ctx, types.NamespacedName{Namespace: nsTmplTier.GetNamespace(), Name: tierTemplateRef}, &tierTemplate); err != nil { + // something went wrong or we haven't found the TierTemplate + return false, err + } + + // check if there is TTR associated with this TierTemplate + ttrCreatedLatest, ttrName, err := r.ensureTTRforTemplate(ctx, nsTmplTier, &tierTemplate) + ttrCreated = ttrCreated || ttrCreatedLatest + if err != nil { + return false, err + } + nsTmplTier.Status.Revisions[tierTemplate.GetName()] = ttrName + } + // TODO handle removal of TierTemplate from NSTemplateTier + // scenario: + // a. TierTemplate is removed/replaced from NSTemplateTier.Spec + // b. NSTemplateTier.Status.Revisions must be cleaned up + // Thus here we should iterate over the Status.Revisions field + // and check if there is any reference to a TierTemplate that is not in the Spec anymore + if ttrCreated { + // we need to update the status.revisions with the new ttrs + logger.Info("ttr created updating status") + if err := r.Client.Status().Update(ctx, nsTmplTier); err != nil { + return ttrCreated, err + } + } + + return ttrCreated, nil +} + +func (r *Reconciler) ensureTTRforTemplate(ctx context.Context, nsTmplTier *toolchainv1alpha1.NSTemplateTier, tierTemplate *toolchainv1alpha1.TierTemplate) (bool, string, error) { + logger := log.FromContext(ctx) + // tierTemplate doesn't support TTRs + // we set TierTemplate as revisions + // TODO this step will be removed once we convert all TierTemplates to TTRs + if tierTemplate.Spec.TemplateObjects == nil { + _, ok := nsTmplTier.Status.Revisions[tierTemplate.GetName()] + if !ok { + return true, tierTemplate.GetName(), nil + } + // nothing to update + return false, "", nil + } + + if tierTemplateRevisionName, found := nsTmplTier.Status.Revisions[tierTemplate.GetName()]; found { + logger.Info("TTR set in the status.revisions for tiertemplate", "tierTemplate.Name", tierTemplate.GetName(), "ttr.Name", tierTemplateRevisionName) + var tierTemplateRevision toolchainv1alpha1.TierTemplateRevision + if err := r.Client.Get(ctx, types.NamespacedName{Namespace: nsTmplTier.GetNamespace(), Name: tierTemplateRevisionName}, &tierTemplateRevision); err != nil { + if errors.IsNotFound(err) { + // no tierTemplateRevision CR was found, + logger.Info("TTR CR not found", "tierTemplateRevision.Name", tierTemplateRevisionName) + // let's create one + ttrName, err := r.createNewTierTemplateRevision(ctx, nsTmplTier, tierTemplate) + return true, ttrName, err + } else { + // something wrong happened + return false, "", err + } + } + // TODO compare TierTemplate content with TTR content + // if the TierTemplate has changes we need to create new TTR + } else { + // no revision was set for this TierTemplate CR, let's create a TTR for it + ttrName, err := r.createNewTierTemplateRevision(ctx, nsTmplTier, tierTemplate) + return true, ttrName, err + } + // nothing changed + return false, "", nil +} + +func (r *Reconciler) createNewTierTemplateRevision(ctx context.Context, nsTmplTier *toolchainv1alpha1.NSTemplateTier, tierTemplate *toolchainv1alpha1.TierTemplate) (string, error) { + ttr := NewTTR(tierTemplate, nsTmplTier) + ttr, err := r.createTTR(ctx, ttr, tierTemplate) + if err != nil { + // something went wrong while creating new ttr + return "", err + } + return ttr.GetName(), nil +} + +// getNSTemplateTierRefs returns a list with all the refs from the NSTemplateTier +func getNSTemplateTierRefs(tmplTier *toolchainv1alpha1.NSTemplateTier) []string { + var refs []string + for _, ns := range tmplTier.Spec.Namespaces { + refs = append(refs, ns.TemplateRef) + } + if tmplTier.Spec.ClusterResources != nil { + refs = append(refs, tmplTier.Spec.ClusterResources.TemplateRef) + } + + roles := make([]string, 0, len(tmplTier.Spec.SpaceRoles)) + for r := range tmplTier.Spec.SpaceRoles { + roles = append(roles, r) + } + for _, r := range roles { + refs = append(refs, tmplTier.Spec.SpaceRoles[r].TemplateRef) + } + return refs +} + +func (r *Reconciler) createTTR(ctx context.Context, ttr *toolchainv1alpha1.TierTemplateRevision, tmplTier *toolchainv1alpha1.TierTemplate) (*toolchainv1alpha1.TierTemplateRevision, error) { + err := r.Client.Create(ctx, ttr) + if err != nil { + return nil, fmt.Errorf("unable to create TierTemplateRevision: %w", err) + } + + logger := log.FromContext(ctx) + logger.Info("created TierTemplateRevision", "tierTemplateRevision.Name", ttr.Name, "tierTemplate.Name", tmplTier.Name) + return ttr, nil +} + +// NewTTR creates a TierTemplateRevision CR for a given TierTemplate object. +func NewTTR(tierTmpl *toolchainv1alpha1.TierTemplate, nsTmplTier *toolchainv1alpha1.NSTemplateTier) *toolchainv1alpha1.TierTemplateRevision { + tierName := nsTmplTier.GetName() + tierTemplateName := tierTmpl.GetName() + labels := map[string]string{ + toolchainv1alpha1.TierLabelKey: tierName, + toolchainv1alpha1.TemplateRefLabelKey: tierTemplateName, + } + + newTTRName := fmt.Sprintf("%s-%s-", tierName, tierTemplateName) + ttr := &toolchainv1alpha1.TierTemplateRevision{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: tierTmpl.GetNamespace(), + GenerateName: newTTRName + "-", + Labels: labels, + }, + Spec: toolchainv1alpha1.TierTemplateRevisionSpec{ + TemplateObjects: tierTmpl.Spec.TemplateObjects, + Parameters: nsTmplTier.Spec.Parameters, // save the parameters from the NSTemplateTier,to detect further changes and for evaluating those in the TemplateObjects when provisioning Spaces. + }, + } + + return ttr +} diff --git a/controllers/nstemplatetier/nstemplatetier_controller_test.go b/controllers/nstemplatetier/nstemplatetier_controller_test.go index 93369a559..1835c8e2b 100644 --- a/controllers/nstemplatetier/nstemplatetier_controller_test.go +++ b/controllers/nstemplatetier/nstemplatetier_controller_test.go @@ -4,19 +4,27 @@ import ( "context" "fmt" "os" + "strings" "testing" + "time" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/host-operator/controllers/nstemplatetier" "github.com/codeready-toolchain/host-operator/pkg/apis" tiertest "github.com/codeready-toolchain/host-operator/test/nstemplatetier" + "github.com/codeready-toolchain/host-operator/test/tiertemplaterevision" "github.com/codeready-toolchain/toolchain-common/pkg/test" - + templatev1 "github.com/openshift/api/template/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/client-go/kubernetes/scheme" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -29,7 +37,6 @@ const ( ) func TestReconcile(t *testing.T) { - // given logf.SetLogger(zap.New(zap.UseDevMode(true))) @@ -40,8 +47,7 @@ func TestReconcile(t *testing.T) { t.Run("tier not found", func(t *testing.T) { // given base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates) - initObjs := []runtimeclient.Object{base1nsTier} - r, req, cl := prepareReconcile(t, base1nsTier.Name, initObjs...) + r, req, cl := prepareReconcile(t, base1nsTier.Name, base1nsTier) cl.MockGet = func(ctx context.Context, key types.NamespacedName, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { if _, ok := obj.(*toolchainv1alpha1.NSTemplateTier); ok { return errors.NewNotFound(schema.GroupResource{}, key.Name) @@ -58,8 +64,7 @@ func TestReconcile(t *testing.T) { t.Run("other error", func(t *testing.T) { // given base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates) - initObjs := []runtimeclient.Object{base1nsTier} - r, req, cl := prepareReconcile(t, base1nsTier.Name, initObjs...) + r, req, cl := prepareReconcile(t, base1nsTier.Name, base1nsTier) cl.MockGet = func(ctx context.Context, key types.NamespacedName, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { if _, ok := obj.(*toolchainv1alpha1.NSTemplateTier); ok { return fmt.Errorf("mock error") @@ -77,6 +82,247 @@ func TestReconcile(t *testing.T) { }) + t.Run("revisions management", func(t *testing.T) { + // given + base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates, + // the tiertemplate revision CR should have a copy of those parameters + tiertest.WithParameter("DEPLOYMENT_QUOTA", "60"), + ) + tierTemplatesRefs := []string{ + "base1ns-admin-123456new", "base1ns-clusterresources-123456new", "base1ns-code-123456new", "base1ns-dev-123456new", "base1ns-edit-123456new", "base1ns-stage-123456new", "base1ns-viewer-123456new", + } + + // TODO remove this subtest once we completely switch to using TTRs + t.Run("using tiertemplates as revisions", func(t *testing.T) { + tierTemplates := initTierTemplates(t, nil, base1nsTier.Name) + t.Run("add revisions when they are missing", func(t *testing.T) { + // given + r, req, cl := prepareReconcile(t, base1nsTier.Name, append(tierTemplates, base1nsTier)...) + // when + res, err := r.Reconcile(context.TODO(), req) + // then + require.NoError(t, err) + require.Equal(t, reconcile.Result{RequeueAfter: time.Second}, res) // explicit requeue after the adding revisions in `status.revisions` + // check that revisions field was populated + tiertest.AssertThatNSTemplateTier(t, "base1ns", cl). + HasStatusTierTemplateRevisions(tierTemplatesRefs) + // check that expected TierTemplateRevision CRs were NOT created when using TierTemplates as revisions + tiertemplaterevision.AssertThatTTRs(t, cl, base1nsTier.GetNamespace()).DoNotExist() + t.Run("don't add revisions when they are up to date", func(t *testing.T) { + // given + // the NSTemplateTier already has the revisions from previous test + + // when + res, err = r.Reconcile(context.TODO(), req) + // then + require.NoError(t, err) + require.Equal(t, reconcile.Result{Requeue: false}, res) // no reconcile + // revisions are the same + tiertest.AssertThatNSTemplateTier(t, "base1ns", cl). + HasStatusTierTemplateRevisions(tierTemplatesRefs) + // no TierTemplateRevision CRs were created + tiertemplaterevision.AssertThatTTRs(t, cl, base1nsTier.GetNamespace()).DoNotExist() + }) + }) + }) + + t.Run("using TTR as revisions", func(t *testing.T) { + // initialize tier templates with templateObjects field populated + // for simplicity we initialize all of them with the same objects + var crq = unstructured.Unstructured{Object: map[string]interface{}{ + "kind": "ClusterResourceQuota", + "metadata": map[string]interface{}{ + "name": "for-{{.SPACE_NAME}}-deployments", + }, + "spec": map[string]interface{}{ + "quota": map[string]interface{}{ + "hard": map[string]interface{}{ + "count/deploymentconfigs.apps": "{{.DEPLOYMENT_QUOTA}}", + "count/deployments.apps": "{{.DEPLOYMENT_QUOTA}}", + "count/pods": "600", + }, + }, + "selector": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "labels": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "toolchain.dev.openshift.com/space": "'{{.SPACE_NAME}}'", + }, + }, + }, + }, + }} + t.Run("add revisions when they are missing ", func(t *testing.T) { + // given + tierTemplates := initTierTemplates(t, withTemplateObjects(crq), base1nsTier.Name) + r, req, cl := prepareReconcile(t, base1nsTier.Name, append(tierTemplates, base1nsTier)...) + // when + res, err := r.Reconcile(context.TODO(), req) + // then + require.NoError(t, err) + require.Equal(t, reconcile.Result{RequeueAfter: time.Second}, res) // explicit requeue after the adding revisions in `status.revisions` + // check that revisions field was populated + tiertest.AssertThatNSTemplateTier(t, "base1ns", cl). + HasStatusTierTemplateRevisions(tierTemplatesRefs) + // check that expected TierTemplateRevision CRs were created + tiertemplaterevision.AssertThatTTRs(t, cl, base1nsTier.GetNamespace()). + ExistFor("base1ns", tierTemplatesRefs...).ForEach(func(ttr *toolchainv1alpha1.TierTemplateRevision) { + // verify the content of the TierTemplate matches the one of the TTR + templateRef, ok := ttr.GetLabels()[toolchainv1alpha1.TemplateRefLabelKey] + assert.True(t, ok) + tierTemplate := toolchainv1alpha1.TierTemplate{} + err := cl.Get(context.TODO(), types.NamespacedName{Name: templateRef, Namespace: base1nsTier.GetNamespace()}, &tierTemplate) + require.NoError(t, err) + assert.Equal(t, tierTemplate.Spec.TemplateObjects, ttr.Spec.TemplateObjects) + assert.Equal(t, ttr.Spec.Parameters, base1nsTier.Spec.Parameters) + }) + t.Run("don't add revisions when they are up to date", func(t *testing.T) { + // given + // the NSTemplateTier already has the revisions from previous test + + // when + res, err = r.Reconcile(context.TODO(), req) + // then + require.NoError(t, err) + require.Equal(t, reconcile.Result{Requeue: false}, res) // no reconcile + // revisions are the same + tiertest.AssertThatNSTemplateTier(t, "base1ns", cl). + HasStatusTierTemplateRevisions(tierTemplatesRefs) + // expected TierTemplateRevision CRs are still there + ttrs := toolchainv1alpha1.TierTemplateRevisionList{} + err = cl.List(context.TODO(), &ttrs, runtimeclient.InNamespace(base1nsTier.GetNamespace())) + require.NoError(t, err) + require.Len(t, ttrs.Items, len(tierTemplatesRefs)) // it's one TTR per each tiertemplate in the NSTemplateTier + t.Run("revision field is set but some TierTemplateRevision is missing, status should be updated", func(t *testing.T) { + // given + // the NSTemplateTier already has the revisions from previous test + // we delete the first TTR to make sure the status is updated and the TTR get's recreated + err = cl.Delete(context.TODO(), &ttrs.Items[0]) + require.NoError(t, err) + + // when + res, err = r.Reconcile(context.TODO(), req) + // then + require.NoError(t, err) + // revisions are the same + tiertest.AssertThatNSTemplateTier(t, "base1ns", cl). + HasStatusTierTemplateRevisions(tierTemplatesRefs) + // expected TierTemplateRevision CRs are there + ttrs := toolchainv1alpha1.TierTemplateRevisionList{} + err = cl.List(context.TODO(), &ttrs, runtimeclient.InNamespace(base1nsTier.GetNamespace())) + require.NoError(t, err) + require.Len(t, ttrs.Items, len(tierTemplatesRefs)) // it's one TTR per each tiertemplate in the NSTemplateTier + }) + }) + + }) + + t.Run("revision field is set but TierTemplateRevision CRs are missing, they should be created", func(t *testing.T) { + // given + // the NSTemplateTier has already the status.revisions field populated + // but the TierTemplateRevision CRs are missing + tierTemplates := initTierTemplates(t, withTemplateObjects(crq), base1nsTier.Name) + base1nsTierWithRevisions := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates, + // the tiertemplate revision CR should have a copy of those parameters + tiertest.WithParameter("DEPLOYMENT_QUOTA", "60"), + ) + initialRevisions := map[string]string{ + "base1ns-admin-123456new": "base1ns-admin-123456new-abcd", + "base1ns-clusterresources-123456new": "base1ns-clusterresources-123456new-abcd", + "base1ns-code-123456new": "base1ns-code-123456new-abcd", + "base1ns-dev-123456new": "base1ns-dev-123456new-abcd", + "base1ns-edit-123456new": "`base1ns-edit-123456new-abcd", + "base1ns-stage-123456new": "base1ns-stage-123456new-abcd", + "base1ns-viewer-123456new": "base1ns-viewer-123456new-abcd", + } + base1nsTierWithRevisions.Status.Revisions = initialRevisions + r, req, cl := prepareReconcile(t, base1nsTierWithRevisions.Name, append(tierTemplates, base1nsTierWithRevisions)...) + // when + // check no TTR is present before reconciling + tiertemplaterevision.AssertThatTTRs(t, cl, base1nsTierWithRevisions.GetNamespace()).DoNotExist() + _, err := r.Reconcile(context.TODO(), req) + // then + require.NoError(t, err) + // check that revisions field was populated + tiertest.AssertThatNSTemplateTier(t, "base1ns", cl). + HasStatusTierTemplateRevisions(tierTemplatesRefs) + // check that expected TierTemplateRevision CRs were created + tiertemplaterevision.AssertThatTTRs(t, cl, base1nsTierWithRevisions.GetNamespace()). + NumberOfPresentCRs(len(tierTemplatesRefs)). // there should be the same amount of TTRs + ForEach(func(ttr *toolchainv1alpha1.TierTemplateRevision) { + // but their name should differ from the ones initially set in the revisions field + assert.NotEqual(t, initialRevisions[ttr.GetLabels()[toolchainv1alpha1.TemplateRefLabelKey]], ttr.GetName()) + }) + }) + + t.Run("TTR name should stay within 63 chars, so that they can be used as labels", func(t *testing.T) { + // given + // the TierTemplateRevision CRs are missing, and their name are based on the tier name. + // Making the TierName already 63chars long we test that the TTR name stays within 63 chars + veryLongTierName := "somerandomstringtomakethenamelongerthan63chars12345678912345678" + tierWithVeryLongName := tiertest.Tier(t, veryLongTierName, tiertest.NSTemplateTierSpecWithTierName(veryLongTierName), + tiertest.WithParameter("DEPLOYMENT_QUOTA", "60"), + ) + tierTemplatesWithLongNames := initTierTemplates(t, withTemplateObjects(crq), tierWithVeryLongName.Name) + r, req, cl := prepareReconcile(t, tierWithVeryLongName.Name, append(tierTemplatesWithLongNames, tierWithVeryLongName)...) + // when + // check no TTR is present before reconciling + tiertemplaterevision.AssertThatTTRs(t, cl, tierWithVeryLongName.GetNamespace()).DoNotExist() + _, err := r.Reconcile(context.TODO(), req) + // then + require.NoError(t, err) + // check that expected TierTemplateRevision CRs were created + // with the expected length + ttrs := toolchainv1alpha1.TierTemplateRevisionList{} + err = cl.List(context.TODO(), &ttrs, runtimeclient.InNamespace(tierWithVeryLongName.GetNamespace())) + require.NoError(t, err) + tiertemplaterevision.AssertThatTTRs(t, cl, tierWithVeryLongName.GetNamespace()). + NumberOfPresentCRs(7). + ForEach(func(ttr *toolchainv1alpha1.TierTemplateRevision) { + assert.Empty(t, validation.IsDNS1123Label(ttr.GetName())) + }) + }) + + t.Run("errors", func(t *testing.T) { + + t.Run("error when TierTemplate is missing ", func(t *testing.T) { + // given + // make sure revisions field is nill before starting the test + base1nsTier.Status.Revisions = nil + r, req, cl := prepareReconcile(t, base1nsTier.Name, base1nsTier) + // when + _, err := r.Reconcile(context.TODO(), req) + // then + // we expect an error caused by the absence of the tiertemplate for the `code` namespace CR + require.ErrorContains(t, err, "tiertemplates.toolchain.dev.openshift.com \"base1ns-code-123456new\" not found") + // the revisions field also should remain empty + tiertest.AssertThatNSTemplateTier(t, "base1ns", cl). + HasNoStatusTierTemplateRevisions() + // and the TierTemplateRevision CRs are not created + tiertemplaterevision.AssertThatTTRs(t, cl, base1nsTier.GetNamespace()).DoNotExist() + }) + + }) + + }) + }) + +} + +// initTierTemplates creates the TierTemplates objects for the base1ns tier +func initTierTemplates(t *testing.T, withTemplateObjects []runtime.RawExtension, tierName string) []runtimeclient.Object { + s := scheme.Scheme + err := apis.AddToScheme(s) + require.NoError(t, err) + clusterResourceTierTemplate := createTierTemplate(t, "clusterresources", withTemplateObjects, tierName) + codeNsTierTemplate := createTierTemplate(t, "code", withTemplateObjects, tierName) + devNsTierTemplate := createTierTemplate(t, "dev", withTemplateObjects, tierName) + stageNsTierTemplate := createTierTemplate(t, "stage", withTemplateObjects, tierName) + adminRoleTierTemplate := createTierTemplate(t, "admin", withTemplateObjects, tierName) + viewerRoleTierTemplate := createTierTemplate(t, "viewer", withTemplateObjects, tierName) + editRoleTierTemplate := createTierTemplate(t, "edit", withTemplateObjects, tierName) + tierTemplates := []runtimeclient.Object{clusterResourceTierTemplate, codeNsTierTemplate, devNsTierTemplate, stageNsTierTemplate, adminRoleTierTemplate, viewerRoleTierTemplate, editRoleTierTemplate} + return tierTemplates } func prepareReconcile(t *testing.T, name string, initObjs ...runtimeclient.Object) (reconcile.Reconciler, reconcile.Request, *test.FakeClient) { @@ -96,3 +342,56 @@ func prepareReconcile(t *testing.T, name string, initObjs ...runtimeclient.Objec }, }, cl } + +func createTierTemplate(t *testing.T, typeName string, withTemplateObjects []runtime.RawExtension, tierName string) *toolchainv1alpha1.TierTemplate { + var ( + ns test.TemplateObject = ` +- apiVersion: v1 + kind: Namespace + metadata: + name: ${SPACE_NAME} +` + spacename test.TemplateParam = ` +- name: SPACE_NAME + value: johnsmith` + ) + s := scheme.Scheme + err := apis.AddToScheme(s) + require.NoError(t, err) + codecFactory := serializer.NewCodecFactory(s) + decoder := codecFactory.UniversalDeserializer() + tmpl := templatev1.Template{} + _, _, err = decoder.Decode([]byte(test.CreateTemplate(test.WithObjects(ns), test.WithParams(spacename))), nil, &tmpl) + require.NoError(t, err) + + revision := "123456new" + // we can set the template field to something empty as it is not relevant for the tests + tt := &toolchainv1alpha1.TierTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.ToLower(fmt.Sprintf("%s-%s-%s", tierName, typeName, revision)), + Namespace: test.HostOperatorNs, + }, + Spec: toolchainv1alpha1.TierTemplateSpec{ + TierName: tierName, + Type: typeName, + Revision: revision, + Template: tmpl, + }, + } + + // just copy the raw objects to the templateObjects field + // TODO this will be removed once we switch on using templateObjects only in the TierTemplates + if withTemplateObjects != nil { + tt.Spec.TemplateObjects = withTemplateObjects + } + + return tt +} + +func withTemplateObjects(templates ...unstructured.Unstructured) []runtime.RawExtension { + var templateObjects []runtime.RawExtension + for i := range templates { + templateObjects = append(templateObjects, runtime.RawExtension{Object: &templates[i]}) + } + return templateObjects +} diff --git a/test/nstemplatetier/assertion.go b/test/nstemplatetier/assertion.go new file mode 100644 index 000000000..93bfae4be --- /dev/null +++ b/test/nstemplatetier/assertion.go @@ -0,0 +1,57 @@ +package nstemplatetier + +import ( + "context" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/codeready-toolchain/toolchain-common/pkg/test" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Assertion an assertion helper for an NSTemplateTier +type Assertion struct { + tier *toolchainv1alpha1.NSTemplateTier + client runtimeclient.Client + namespacedName types.NamespacedName + t test.T +} + +func (a *Assertion) loadResource() error { + tier := &toolchainv1alpha1.NSTemplateTier{} + err := a.client.Get(context.TODO(), a.namespacedName, tier) + a.tier = tier + return err +} + +// AssertThatNSTemplateTier helper func to begin with the assertions on an NSTemplateTier +func AssertThatNSTemplateTier(t test.T, name string, client runtimeclient.Client) *Assertion { + return &Assertion{ + client: client, + namespacedName: test.NamespacedName(test.HostOperatorNs, name), + t: t, + } +} + +// HasStatusTierTemplateRevisions verifies revisions for the given TierTemplates are set in the `NSTemplateTier.Status.Revisions` +func (a *Assertion) HasStatusTierTemplateRevisions(revisions []string) *Assertion { + err := a.loadResource() + require.NoError(a.t, err) + // check that each TierTemplate REF has a TierTemplateRevision set + for _, tierTemplateRef := range revisions { + require.NotNil(a.t, a.tier.Status.Revisions) + _, ok := a.tier.Status.Revisions[tierTemplateRef] + require.True(a.t, ok) + } + return a +} + +// HasNoStatusTierTemplateRevisions verifies revisions are not set for in the `NSTemplateTier.Status.Revisions` +func (a *Assertion) HasNoStatusTierTemplateRevisions() *Assertion { + err := a.loadResource() + require.NoError(a.t, err) + require.Nil(a.t, a.tier.Status.Revisions) + return a +} diff --git a/test/nstemplatetier/assertions.go b/test/nstemplatetier/assertions.go deleted file mode 100644 index 094784794..000000000 --- a/test/nstemplatetier/assertions.go +++ /dev/null @@ -1,35 +0,0 @@ -package nstemplatetier - -import ( - "context" - - toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" - "github.com/codeready-toolchain/toolchain-common/pkg/test" - - "k8s.io/apimachinery/pkg/types" - runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Assertion an assertion helper for an NSTemplateTier -type Assertion struct { - tier *toolchainv1alpha1.NSTemplateTier //nolint:golint,unused - client runtimeclient.Client - namespacedName types.NamespacedName - t test.T -} - -func (a *Assertion) loadResource() error { //nolint:golint,unused - tier := &toolchainv1alpha1.NSTemplateTier{} - err := a.client.Get(context.TODO(), a.namespacedName, tier) - a.tier = tier - return err -} - -// AssertThatNSTemplateTier helper func to begin with the assertions on an NSTemplateTier -func AssertThatNSTemplateTier(t test.T, name string, client runtimeclient.Client) *Assertion { - return &Assertion{ - client: client, - namespacedName: test.NamespacedName(test.HostOperatorNs, name), - t: t, - } -} diff --git a/test/nstemplatetier/nstemplatetier.go b/test/nstemplatetier/nstemplatetier.go index 931baabb1..17ae3852a 100644 --- a/test/nstemplatetier/nstemplatetier.go +++ b/test/nstemplatetier/nstemplatetier.go @@ -99,6 +99,36 @@ var CurrentBase1nsTemplates = toolchainv1alpha1.NSTemplateTierSpec{ }, } +func NSTemplateTierSpecWithTierName(tierName string) toolchainv1alpha1.NSTemplateTierSpec { + return toolchainv1alpha1.NSTemplateTierSpec{ + Namespaces: []toolchainv1alpha1.NSTemplateTierNamespace{ + { + TemplateRef: tierName + "-code-123456new", + }, + { + TemplateRef: tierName + "-dev-123456new", + }, + { + TemplateRef: tierName + "-stage-123456new", + }, + }, + ClusterResources: &toolchainv1alpha1.NSTemplateTierClusterResources{ + TemplateRef: tierName + "-clusterresources-123456new", + }, + SpaceRoles: map[string]toolchainv1alpha1.NSTemplateTierSpaceRole{ + "admin": { + TemplateRef: tierName + "-admin-123456new", + }, + "edit": { + TemplateRef: tierName + "-edit-123456new", + }, + "viewer": { + TemplateRef: tierName + "-viewer-123456new", + }, + }, + } +} + // AppStudioEnvTemplates current templates for the "appstudio-env" tier var AppStudioEnvTemplates = toolchainv1alpha1.NSTemplateTierSpec{ Namespaces: []toolchainv1alpha1.NSTemplateTierNamespace{ @@ -178,6 +208,21 @@ func WithoutClusterResources() TierOption { } } +// WithParameter appends a parameter to the parameter's list +func WithParameter(name, value string) TierOption { + return func(tier *toolchainv1alpha1.NSTemplateTier) { + if tier.Spec.Parameters == nil { + tier.Spec.Parameters = []toolchainv1alpha1.Parameter{} + } + tier.Spec.Parameters = append(tier.Spec.Parameters, + toolchainv1alpha1.Parameter{ + Name: name, + Value: value, + }, + ) + } +} + // OtherTier returns an "other" NSTemplateTier func OtherTier() *toolchainv1alpha1.NSTemplateTier { return &toolchainv1alpha1.NSTemplateTier{ diff --git a/test/tiertemplaterevision/assertion.go b/test/tiertemplaterevision/assertion.go new file mode 100644 index 000000000..85b84f7d7 --- /dev/null +++ b/test/tiertemplaterevision/assertion.go @@ -0,0 +1,71 @@ +package tiertemplaterevision + +import ( + "context" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/codeready-toolchain/toolchain-common/pkg/test" + + "github.com/stretchr/testify/require" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Assertion an assertion helper for an TierTemplateRevision +type Assertion struct { + client runtimeclient.Client + namespace string + t test.T +} + +func (a *Assertion) loadResources(labels map[string]string) ([]toolchainv1alpha1.TierTemplateRevision, error) { + ttrs := &toolchainv1alpha1.TierTemplateRevisionList{} + err := a.client.List(context.TODO(), ttrs, runtimeclient.InNamespace(a.namespace), runtimeclient.MatchingLabels(labels)) + return ttrs.Items, err +} + +// AssertThatTTRs helper func to begin with the assertions on an TierTemplateRevisions +func AssertThatTTRs(t test.T, client runtimeclient.Client, namespace string) *Assertion { + return &Assertion{ + client: client, + namespace: namespace, + t: t, + } +} + +// DoNotExist verifies that there is no TTR in the given namespace +func (a *Assertion) DoNotExist() *Assertion { + ttrs, err := a.loadResources(nil) + require.NoError(a.t, err) + require.Empty(a.t, ttrs) + return a +} + +// ExistFor verifies that there are TTRs with given labels +func (a *Assertion) ExistFor(tierName string, tierTemplateRef ...string) *Assertion { + for _, templateRef := range tierTemplateRef { + labels := map[string]string{ + toolchainv1alpha1.TierLabelKey: tierName, + toolchainv1alpha1.TemplateRefLabelKey: templateRef, + } + ttrs, err := a.loadResources(labels) + require.NoError(a.t, err) + require.Len(a.t, ttrs, 1) + } + return a +} + +func (a *Assertion) ForEach(assertionFunc func(ttr *toolchainv1alpha1.TierTemplateRevision)) *Assertion { + ttrs, err := a.loadResources(nil) + require.NoError(a.t, err) + for i := range ttrs { + assertionFunc(&ttrs[i]) + } + return a +} + +func (a *Assertion) NumberOfPresentCRs(expected int) *Assertion { + ttrs, err := a.loadResources(nil) + require.NoError(a.t, err) + require.Len(a.t, ttrs, expected) + return a +}