Skip to content

Commit 47ac084

Browse files
feat: create new ttr for tiertemplate updates (#1102)
* create new TTR when TierTemplate or NSTemplateTier changes --------- Co-authored-by: Matous Jobanek <mjobanek@redhat.com>
1 parent 78c7ae1 commit 47ac084

3 files changed

Lines changed: 231 additions & 54 deletions

File tree

test/e2e/parallel/nstemplatetier_test.go

Lines changed: 127 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"k8s.io/apimachinery/pkg/runtime"
1414

1515
"github.com/gofrs/uuid"
16+
"k8s.io/client-go/kubernetes/scheme"
1617

1718
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
1819
"github.com/codeready-toolchain/toolchain-common/pkg/states"
@@ -21,8 +22,6 @@ import (
2122
. "github.com/codeready-toolchain/toolchain-e2e/testsupport/space"
2223
"github.com/codeready-toolchain/toolchain-e2e/testsupport/tiers"
2324
"github.com/codeready-toolchain/toolchain-e2e/testsupport/wait"
24-
apiwait "k8s.io/apimachinery/pkg/util/wait"
25-
2625
"github.com/stretchr/testify/assert"
2726
"github.com/stretchr/testify/require"
2827
"k8s.io/apimachinery/pkg/labels"
@@ -378,76 +377,125 @@ func TestTierTemplateRevision(t *testing.T) {
378377
require.NoError(t, err)
379378
// for the tiertemplaterevisions to be created the tiertemplates need to have template objects populated
380379
// we add the RawExtension objects to the TemplateObjects field
380+
crq := getTestCRQ("600")
381+
rawTemplateObjects := []runtime.RawExtension{{Object: &crq}}
382+
updateTierTemplateObjects := func(template *toolchainv1alpha1.TierTemplate) error {
383+
template.Spec.TemplateObjects = rawTemplateObjects
384+
return nil
385+
}
386+
// for simplicity, we add the CRQ to all types of templates (both cluster scope and namespace scoped),
387+
// even if the CRQ is cluster scoped.
388+
// WARNING: thus THIS NSTemplateTier should NOT be used to provision a user!!!
389+
customTier := tiers.CreateCustomNSTemplateTier(t, hostAwait, "ttr", baseTier,
390+
tiers.WithNamespaceResources(t, baseTier, updateTierTemplateObjects),
391+
tiers.WithClusterResources(t, baseTier, updateTierTemplateObjects),
392+
tiers.WithSpaceRoles(t, baseTier, updateTierTemplateObjects),
393+
tiers.WithParameter("DEPLOYMENT_QUOTA", "60"))
394+
// when
395+
// we verify that TierTemplateRevision CRs were created, since all the tiertemplates now have templateObjects field populated
396+
tier, err := hostAwait.WaitForNSTemplateTierAndCheckTemplates(t, "ttr",
397+
wait.HasStatusTierTemplateRevisions(tiers.GetTemplateRefs(t, hostAwait, "ttr").Flatten()))
398+
require.NoError(t, err)
399+
customTier.NSTemplateTier = tier
400+
401+
// then
402+
// check the expected total number of ttr matches,
403+
// we IDEALLY expect one TTR per each tiertemplate to be created (clusterresource, namespace and spacerole), thus a total of 3 TTRs ideally.
404+
// But since the creation of a TTR could be very quick and could trigger another reconcile of the NSTemplateTier before the status is actually updated with the reference,
405+
// this might generate some copies of the TTRs. This is not a problem in production since the cleanup mechanism of TTRs will remove the extra ones but could cause some flakiness with the test,
406+
// thus we assert the number of TTRs doesn't exceed the double of the expected number.
407+
// TODO check for exact match or remove the *2 and check for not empty revisions list, once we implement the cleanup controller
408+
ttrs, err := hostAwait.WaitForTTRs(t, customTier.Name, wait.LessOrEqual(len(tiers.GetTemplateRefs(t, hostAwait, "ttr").Flatten())*2))
409+
require.NoError(t, err)
410+
411+
t.Run("update of tiertemplate should trigger creation of new TTR", func(t *testing.T) {
412+
// given
413+
// that the tiertemplates and nstemlpatetier are provisioned from the parent test
414+
ttrToBeModified, found := customTier.Status.Revisions[customTier.Spec.ClusterResources.TemplateRef]
415+
require.True(t, found)
416+
// check that it has the crq before updating it
417+
checkThatTTRContainsCRQ(t, ttrToBeModified, ttrs, crq)
418+
419+
// when
420+
// we update one tiertemplate
421+
// let's reduce the pod count
422+
updatedCRQ := getTestCRQ("100")
423+
_, err = wait.For(t, hostAwait.Awaitility, &toolchainv1alpha1.TierTemplate{}).
424+
Update(customTier.Spec.ClusterResources.TemplateRef, hostAwait.Namespace, func(tiertemplate *toolchainv1alpha1.TierTemplate) {
425+
tiertemplate.Spec.TemplateObjects = []runtime.RawExtension{{Object: &updatedCRQ}}
426+
})
427+
require.NoError(t, err)
428+
429+
// then
430+
// a new TTR was created
431+
// TODO check for exact match or remove the +1 once we implement the cleanup controller
432+
updatedTTRs, err := hostAwait.WaitForTTRs(t, customTier.Name, wait.GreaterOrEqual(len(ttrs)+1))
433+
require.NoError(t, err)
434+
// get the updated nstemplatetier
435+
updatedCustomTier, err := hostAwait.WaitForNSTemplateTier(t, customTier.Name)
436+
newTTR, found := updatedCustomTier.Status.Revisions[updatedCustomTier.Spec.ClusterResources.TemplateRef]
437+
require.True(t, found)
438+
// check that it has the updated crq
439+
checkThatTTRContainsCRQ(t, newTTR, updatedTTRs, updatedCRQ)
440+
441+
t.Run("update of the NSTemplateTier parameters should trigger creation of new TTR", func(t *testing.T) {
442+
// given
443+
// that the TierTemplates and NSTemplateTier are provisioned from the parent test
444+
// and they have the initial parameter value for deployment quota
445+
checkThatTTRsHaveParameter(t, customTier, updatedTTRs, toolchainv1alpha1.Parameter{
446+
Name: "DEPLOYMENT_QUOTA",
447+
Value: "60",
448+
})
449+
450+
// when
451+
// we increase the parameter for the deployment quota
452+
customTier = tiers.UpdateCustomNSTemplateTier(t, hostAwait, customTier, tiers.WithParameter("DEPLOYMENT_QUOTA", "100"))
453+
require.NoError(t, err)
454+
455+
// then
456+
// an additional TTR will be created
457+
// TODO check for exact match or remove the +1 once we implement the cleanup controller
458+
ttrsWithNewParams, err := hostAwait.WaitForTTRs(t, customTier.Name, wait.GreaterOrEqual(len(updatedTTRs)+1))
459+
require.NoError(t, err)
460+
// retrieve new tier once the ttrs were created and the revision field updated
461+
customTier.NSTemplateTier, err = hostAwait.WaitForNSTemplateTier(t, customTier.Name)
462+
require.NoError(t, err)
463+
// and the parameter is updated in all the ttrs
464+
checkThatTTRsHaveParameter(t, customTier, ttrsWithNewParams, toolchainv1alpha1.Parameter{
465+
Name: "DEPLOYMENT_QUOTA",
466+
Value: "100",
467+
})
468+
})
469+
470+
})
471+
472+
}
473+
474+
func getTestCRQ(podsCount string) unstructured.Unstructured {
381475
crq := unstructured.Unstructured{Object: map[string]interface{}{
382476
"kind": "ClusterResourceQuota",
383477
"metadata": map[string]interface{}{
384478
"name": "for-{{.SPACE_NAME}}-deployments",
385479
},
386480
"spec": map[string]interface{}{
387481
"quota": map[string]interface{}{
388-
"hard": map[string]string{
482+
"hard": map[string]interface{}{
389483
"count/deploymentconfigs.apps": "{{.DEPLOYMENT_QUOTA}}",
390484
"count/deployments.apps": "{{.DEPLOYMENT_QUOTA}}",
391-
"count/pods": "600",
485+
"count/pods": podsCount,
392486
},
393487
},
394488
"selector": map[string]interface{}{
395-
"annotations": map[string]string{},
489+
"annotations": map[string]interface{}{},
396490
"labels": map[string]interface{}{
397-
"matchLabels": map[string]string{
491+
"matchLabels": map[string]interface{}{
398492
"toolchain.dev.openshift.com/space": "{{.SPACE_NAME}}",
399493
},
400494
},
401495
},
402496
},
403497
}}
404-
rawTemplateObjects := []runtime.RawExtension{{Object: &crq}}
405-
updateTierTemplateObjects := func(template *toolchainv1alpha1.TierTemplate) error {
406-
template.Spec.TemplateObjects = rawTemplateObjects
407-
return nil
408-
}
409-
// for simplicity, we add the CRQ to all types of templates (both cluster scope and namespace scoped),
410-
// even if the CRQ is cluster scoped.
411-
// WARNING: thus THIS NSTemplateTier should NOT be sued to provision a user!!!
412-
tiers.CreateCustomNSTemplateTier(t, hostAwait, "ttr", baseTier,
413-
tiers.WithNamespaceResources(t, baseTier, updateTierTemplateObjects),
414-
tiers.WithClusterResources(t, baseTier, updateTierTemplateObjects),
415-
tiers.WithSpaceRoles(t, baseTier, updateTierTemplateObjects),
416-
tiers.WithParameter("DEPLOYMENT_QUOTA", "60"))
417-
// when
418-
// we verify the counters in the status.history for 'tierUsingTierTemplateRevisions' tier
419-
// and verify that TierTemplateRevision CRs were created, since all the tiertemplates now have templateObjects field populated
420-
customTier, err := hostAwait.WaitForNSTemplateTierAndCheckTemplates(t, "ttr",
421-
wait.HasStatusTierTemplateRevisions(tiers.GetTemplateRefs(t, hostAwait, "ttr").Flatten()))
422-
require.NoError(t, err)
423-
424-
// then
425-
// check the expected total number of ttr matches
426-
err = apiwait.PollUntilContextTimeout(context.TODO(), hostAwait.RetryInterval, hostAwait.Timeout, true, func(ctx context.Context) (done bool, err error) {
427-
objs := &toolchainv1alpha1.TierTemplateRevisionList{}
428-
if err := hostAwait.Client.List(ctx, objs, client.InNamespace(hostAwait.Namespace)); err != nil {
429-
return false, err
430-
}
431-
// we IDEALLY expect one TTR per each tiertemplate to be created (clusterresource, namespace and spacerole), thus a total of 3 TTRs ideally.
432-
// But since the creation of a TTR could be very quick and could trigger another reconcile of the NSTemplateTier before the status is actually updated with the reference,
433-
// this might generate some copies of the TTRs. This is not a problem in production since the cleanup mechanism of TTRs will remove the extra ones but could cause some flakiness with the test,
434-
// thus we assert the number of TTRs doesn't exceed the double of the expected number.
435-
assert.LessOrEqual(t, len(objs.Items), len(tiers.GetTemplateRefs(t, hostAwait, "ttr").Flatten())*2)
436-
// we check that the TTR content has the parameters replaced with values from the NSTemplateTier
437-
for _, obj := range objs.Items {
438-
// the object should have all the variables still there since this one will be replaced when provisioning the Space
439-
assert.Contains(t, string(obj.Spec.TemplateObjects[0].Raw), ".SPACE_NAME")
440-
assert.Contains(t, string(obj.Spec.TemplateObjects[0].Raw), ".DEPLOYMENT_QUOTA")
441-
// the parameter is copied from the NSTemplateTier
442-
assert.NotNil(t, obj.Spec.Parameters)
443-
assert.NotNil(t, customTier.Spec.Parameters)
444-
// we only expect the static parameter DEPLOYMENT_QUOTA to be copied from the tier to the TTR.
445-
// the SPACE_NAME is not a parameter, but a dynamic variable which will be evaluated when provisioning a namespace for the user.
446-
assert.ElementsMatch(t, customTier.Spec.Parameters, obj.Spec.Parameters)
447-
}
448-
return true, nil
449-
})
450-
require.NoError(t, err)
498+
return crq
451499
}
452500

453501
func withClusterRoleBindings(t *testing.T, otherTier *toolchainv1alpha1.NSTemplateTier, feature string) tiers.CustomNSTemplateTierModifier {
@@ -498,3 +546,29 @@ const (
498546
}
499547
`
500548
)
549+
550+
// checkThatTTRContainsCRQ verifies if a given ttr from the list contains the CRQ in the templateObjects field
551+
func checkThatTTRContainsCRQ(t *testing.T, ttrName string, ttrs []toolchainv1alpha1.TierTemplateRevision, crq unstructured.Unstructured) {
552+
for _, ttr := range ttrs {
553+
if ttr.Name == ttrName {
554+
assert.NotEmpty(t, ttr.Spec.TemplateObjects)
555+
unstructuredObj := &unstructured.Unstructured{}
556+
_, _, err := scheme.Codecs.UniversalDeserializer().Decode(ttr.Spec.TemplateObjects[0].Raw, nil, unstructuredObj)
557+
require.NoError(t, err)
558+
assert.Equal(t, &crq, unstructuredObj)
559+
return
560+
}
561+
}
562+
require.FailNowf(t, "Unable to find a TTR with required crq", "ttr:%s CRQ:%+v", ttrName, crq)
563+
}
564+
565+
// checkThatTTRsHaveParameter verifies that ttrs from the list have the required parameter
566+
func checkThatTTRsHaveParameter(t *testing.T, tier *tiers.CustomNSTemplateTier, ttrs []toolchainv1alpha1.TierTemplateRevision, parameters toolchainv1alpha1.Parameter) {
567+
for _, ttr := range ttrs {
568+
// if the ttr is still in the revisions field we check that it contains the required parameters
569+
if ttrNameInRev, ttrFound := tier.Status.Revisions[ttr.GetLabels()[toolchainv1alpha1.TemplateRefLabelKey]]; ttrFound && ttrNameInRev == ttr.Name {
570+
assert.Contains(t, ttr.Spec.Parameters, parameters, "Unable to find required parameters:%+v in the TTR:%s", parameters, ttr.Name)
571+
return
572+
}
573+
}
574+
}

testsupport/tiers/tier_setup.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ func WithParameter(name, value string) CustomNSTemplateTierModifier {
8686
if tier.Spec.Parameters == nil {
8787
tier.Spec.Parameters = []toolchainv1alpha1.Parameter{}
8888
}
89+
90+
for i, param := range tier.Spec.Parameters {
91+
if param.Name == name {
92+
// if the param already exists, let's set the new value
93+
tier.Spec.Parameters[i].Value = value
94+
return nil
95+
}
96+
}
97+
// if it's a new parameter, let's append it to the existing ones
8998
tier.Spec.Parameters = append(tier.Spec.Parameters,
9099
toolchainv1alpha1.Parameter{
91100
Name: name,
@@ -143,7 +152,7 @@ func UpdateCustomNSTemplateTier(t *testing.T, hostAwait *wait.HostAwaitility, ti
143152
err := modify(hostAwait, tier)
144153
require.NoError(t, err)
145154
}
146-
_, err = wait.For(t, hostAwait.Awaitility, &toolchainv1alpha1.NSTemplateTier{}).
155+
tier.NSTemplateTier, err = wait.For(t, hostAwait.Awaitility, &toolchainv1alpha1.NSTemplateTier{}).
147156
Update(tier.NSTemplateTier.Name, hostAwait.Namespace, func(nstt *toolchainv1alpha1.NSTemplateTier) {
148157
nstt.Spec = tier.NSTemplateTier.Spec
149158
})

testsupport/wait/host.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,100 @@ func (a *HostAwaitility) WaitForTierTemplate(t *testing.T, name string) (*toolch
994994
return tierTemplate, err
995995
}
996996

997+
// TierTemplateRevisionWaitCriterion a struct to compare with an expected TierTemplateRevision
998+
type TierTemplateRevisionWaitCriterion struct {
999+
Match func([]toolchainv1alpha1.TierTemplateRevision) bool
1000+
Diff func([]toolchainv1alpha1.TierTemplateRevision) string
1001+
}
1002+
1003+
func matchTierTemplateRevisionWaitCriterion(actual []toolchainv1alpha1.TierTemplateRevision, criteria ...TierTemplateRevisionWaitCriterion) bool {
1004+
for _, c := range criteria {
1005+
// if at least one criteria does not match, keep waiting
1006+
if !c.Match(actual) {
1007+
return false
1008+
}
1009+
}
1010+
return true
1011+
}
1012+
1013+
func (a *HostAwaitility) printTierTemplateRevisionWaitCriterionDiffs(t *testing.T, actual []toolchainv1alpha1.TierTemplateRevision, tierName string, criteria ...TierTemplateRevisionWaitCriterion) {
1014+
buf := &strings.Builder{}
1015+
if len(actual) == 0 {
1016+
buf.WriteString("no ttrs found\n")
1017+
} else {
1018+
buf.WriteString("failed to find ttrs with matching criteria:\n")
1019+
buf.WriteString("actual:\n")
1020+
for _, obj := range actual {
1021+
y, _ := StringifyObject(&obj) // nolint:gosec
1022+
buf.Write(y)
1023+
}
1024+
buf.WriteString("\n----\n")
1025+
buf.WriteString("diffs:\n")
1026+
for _, c := range criteria {
1027+
if !c.Match(actual) {
1028+
buf.WriteString(c.Diff(actual))
1029+
buf.WriteString("\n")
1030+
}
1031+
}
1032+
}
1033+
opts := client.MatchingLabels(map[string]string{
1034+
toolchainv1alpha1.TierLabelKey: tierName,
1035+
})
1036+
// include also all TierTemplateRevisions for the given tier, to help troubleshooting
1037+
a.listAndPrint(t, "TierTemplateRevisions", a.Namespace, &toolchainv1alpha1.TierTemplateRevisionList{}, opts)
1038+
// include also all TierTemplate for the given tiertemplate revisions, to help troubleshooting
1039+
for _, ttr := range actual {
1040+
a.GetAndPrint(t, "TierTemplate", a.Namespace, ttr.GetLabels()[toolchainv1alpha1.TemplateRefLabelKey], &toolchainv1alpha1.TierTemplate{})
1041+
}
1042+
1043+
t.Log(buf.String())
1044+
}
1045+
1046+
// GreaterOrEqual checks if the number of TTRs is greater or equal than the expected one
1047+
func GreaterOrEqual(count int) TierTemplateRevisionWaitCriterion {
1048+
return TierTemplateRevisionWaitCriterion{
1049+
Match: func(actual []toolchainv1alpha1.TierTemplateRevision) bool {
1050+
return len(actual) >= count
1051+
},
1052+
Diff: func(actual []toolchainv1alpha1.TierTemplateRevision) string {
1053+
return fmt.Sprintf("number of ttrs %d is not greater or equal than %d \n", len(actual), count)
1054+
},
1055+
}
1056+
}
1057+
1058+
// LessOrEqual checks if the number of TTRs is less or equal than the expected one
1059+
func LessOrEqual(count int) TierTemplateRevisionWaitCriterion {
1060+
return TierTemplateRevisionWaitCriterion{
1061+
Match: func(actual []toolchainv1alpha1.TierTemplateRevision) bool {
1062+
return len(actual) <= count
1063+
},
1064+
Diff: func(actual []toolchainv1alpha1.TierTemplateRevision) string {
1065+
return fmt.Sprintf("number of ttrs %d is not less or equal than %d \n", len(actual), count)
1066+
},
1067+
}
1068+
}
1069+
1070+
func (a *HostAwaitility) WaitForTTRs(t *testing.T, tierName string, criteria ...TierTemplateRevisionWaitCriterion) ([]toolchainv1alpha1.TierTemplateRevision, error) {
1071+
t.Logf("waiting for ttrs to match criteria for tier '%s'", tierName)
1072+
var ttrs []toolchainv1alpha1.TierTemplateRevision
1073+
err := wait.PollUntilContextTimeout(context.TODO(), a.RetryInterval, a.Timeout, true, func(ctx context.Context) (done bool, err error) {
1074+
objs := &toolchainv1alpha1.TierTemplateRevisionList{}
1075+
if err := a.Client.List(ctx, objs, client.InNamespace(a.Namespace), client.MatchingLabels{toolchainv1alpha1.TierLabelKey: tierName}); err != nil {
1076+
return false, err
1077+
}
1078+
if len(objs.Items) == 0 {
1079+
return false, nil
1080+
}
1081+
ttrs = objs.Items
1082+
return matchTierTemplateRevisionWaitCriterion(ttrs, criteria...), nil
1083+
})
1084+
// no match found, print the diffs
1085+
if err != nil {
1086+
a.printTierTemplateRevisionWaitCriterionDiffs(t, ttrs, tierName, criteria...)
1087+
}
1088+
return ttrs, err
1089+
}
1090+
9971091
// NSTemplateTierWaitCriterion a struct to compare with an expected NSTemplateTier
9981092
type NSTemplateTierWaitCriterion struct {
9991093
Match func(*toolchainv1alpha1.NSTemplateTier) bool

0 commit comments

Comments
 (0)