github.com/opendevstack/tailor@v1.3.5-0.20220119161809-cab064e60a67/pkg/openshift/changeset_test.go (about)

     1  package openshift
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/opendevstack/tailor/internal/test/helper"
     9  )
    10  
    11  func TestNewChangesetCreationOfResources(t *testing.T) {
    12  	tests := map[string]struct {
    13  		templateFixture string
    14  		expectedGolden  string
    15  	}{
    16  		"Without annotations": {
    17  			templateFixture: "is.yml",
    18  			expectedGolden:  "is.yml",
    19  		},
    20  		"With annotations": {
    21  			templateFixture: "is-annotation.yml",
    22  			expectedGolden:  "is-annotation.yml",
    23  		},
    24  		"With image reference": {
    25  			templateFixture: "dc.yml",
    26  			expectedGolden:  "dc.yml",
    27  		},
    28  		"With image reference and annotation": {
    29  			templateFixture: "dc-annotation.yml",
    30  			expectedGolden:  "dc-annotation.yml",
    31  		},
    32  	}
    33  
    34  	for name, tc := range tests {
    35  		t.Run(name, func(t *testing.T) {
    36  			filter, err := NewResourceFilter("", "", []string{})
    37  			if err != nil {
    38  				t.Fatal(err)
    39  			}
    40  			platformBasedList, err := NewPlatformBasedResourceList(
    41  				filter,
    42  				[]byte(""), // empty to ensure creation of resource
    43  			)
    44  			if err != nil {
    45  				t.Fatal(err)
    46  			}
    47  			templateBasedList, err := NewTemplateBasedResourceList(
    48  				filter,
    49  				helper.ReadFixtureFile(t, "templates/"+tc.templateFixture),
    50  			)
    51  			if err != nil {
    52  				t.Fatal(err)
    53  			}
    54  			upsertOnly := false
    55  			allowRecreate := false
    56  			preservePaths := []string{}
    57  			cs, err := NewChangeset(
    58  				platformBasedList,
    59  				templateBasedList,
    60  				upsertOnly,
    61  				allowRecreate,
    62  				preservePaths,
    63  			)
    64  			if err != nil {
    65  				t.Fatal(err)
    66  			}
    67  			createChanges := cs.Create
    68  			numberOfCreateChanges := len(createChanges)
    69  			if numberOfCreateChanges != 1 {
    70  				t.Fatalf("Expected one creation change, got: %d", numberOfCreateChanges)
    71  			}
    72  			createChange := createChanges[0]
    73  			want := string(helper.ReadGoldenFile(t, "desired-state/"+tc.expectedGolden))
    74  			got := createChange.DesiredState
    75  			if diff := cmp.Diff(want, got); diff != "" {
    76  				t.Fatalf("Desired state mismatch (-want +got):\n%s", diff)
    77  			}
    78  		})
    79  	}
    80  }
    81  
    82  func TestCalculateChangesManagedAnnotations(t *testing.T) {
    83  
    84  	tests := map[string]struct {
    85  		platformFixture        string
    86  		templateFixture        string
    87  		expectedAction         string
    88  		expectedDiffGoldenFile string
    89  	}{
    90  		"Without annotations": {
    91  			platformFixture: "is-platform",
    92  			templateFixture: "is-template",
    93  			expectedAction:  "Noop",
    94  		},
    95  		"Present in template, not in platform": {
    96  			platformFixture:        "is-platform",
    97  			templateFixture:        "is-template-annotation",
    98  			expectedAction:         "Update",
    99  			expectedDiffGoldenFile: "present-in-template-not-in-platform",
   100  		},
   101  		"Present in platform, not in template": {
   102  			platformFixture:        "is-platform-annotation",
   103  			templateFixture:        "is-template",
   104  			expectedAction:         "Update",
   105  			expectedDiffGoldenFile: "present-in-platform-not-in-template",
   106  		},
   107  		"Present in both": {
   108  			platformFixture: "is-platform-annotation",
   109  			templateFixture: "is-template-annotation",
   110  			expectedAction:  "Noop",
   111  		},
   112  		"Present in platform, changed in template": {
   113  			platformFixture:        "is-platform-annotation",
   114  			templateFixture:        "is-template-annotation-changed",
   115  			expectedAction:         "Update",
   116  			expectedDiffGoldenFile: "present-in-platform-changed-in-template",
   117  		},
   118  		"Present in platform, different key in template": {
   119  			platformFixture:        "is-platform-annotation",
   120  			templateFixture:        "is-template-different-annotation",
   121  			expectedAction:         "Update",
   122  			expectedDiffGoldenFile: "present-in-platform-different-key-in-template",
   123  		},
   124  		"Unmanaged in platform added to template": {
   125  			platformFixture: "is-platform-unmanaged",
   126  			templateFixture: "is-template-annotation",
   127  			expectedAction:  "Noop",
   128  		},
   129  		"Unmanaged in platform, none in template": {
   130  			platformFixture: "is-platform-unmanaged",
   131  			templateFixture: "is-template",
   132  			expectedAction:  "Noop",
   133  		},
   134  		"Unmanaged in platform, none in template, and other change in template": {
   135  			platformFixture:        "is-platform-unmanaged",
   136  			templateFixture:        "is-template-other-change",
   137  			expectedAction:         "Update",
   138  			expectedDiffGoldenFile: "unmanaged-in-platform-none-in-template-other-change-in-template",
   139  		},
   140  	}
   141  
   142  	for name, tc := range tests {
   143  		t.Run(name, func(t *testing.T) {
   144  			platformItem := getPlatformItem(t, "item-managed-annotations/"+tc.platformFixture+".yml")
   145  			templateItem := getTemplateItem(t, "item-managed-annotations/"+tc.templateFixture+".yml")
   146  			changes, err := calculateChanges(templateItem, platformItem, []string{}, true)
   147  			if err != nil {
   148  				t.Fatal(err)
   149  			}
   150  			if len(changes) != 1 {
   151  				t.Fatalf("Expected 1 change, got: %d", len(changes))
   152  			}
   153  			actualChange := changes[0]
   154  			if actualChange.Action != tc.expectedAction {
   155  				t.Fatalf("Expected change action to be: %s, got: %s", tc.expectedAction, actualChange.Action)
   156  			}
   157  			if len(tc.expectedDiffGoldenFile) > 0 {
   158  				want := strings.TrimSpace(getGoldenDiff(t, "item-managed-annotations", tc.expectedDiffGoldenFile+".txt"))
   159  				got := strings.TrimSpace(actualChange.Diff(true))
   160  				if diff := cmp.Diff(want, got); diff != "" {
   161  					t.Errorf("Change diff mismatch (-want +got):\n%s", diff)
   162  				}
   163  			}
   164  		})
   165  	}
   166  }
   167  
   168  func TestCalculateChangesAppliedConfiguration(t *testing.T) {
   169  
   170  	tests := map[string]struct {
   171  		platformFixture string
   172  		templateFixture string
   173  		expectedAction  string
   174  	}{
   175  		"Without annotation in platform": {
   176  			platformFixture: "dc-platform",
   177  			templateFixture: "dc-template",
   178  			expectedAction:  "Update",
   179  		},
   180  		"With annotation in platform": {
   181  			platformFixture: "dc-platform-annotation-other",
   182  			templateFixture: "dc-template",
   183  			expectedAction:  "Update",
   184  		},
   185  		"Present in platform": {
   186  			platformFixture: "dc-platform-annotation-applied",
   187  			templateFixture: "dc-template",
   188  			expectedAction:  "Noop",
   189  		},
   190  		"Old Tailor annotation present in platform": {
   191  			platformFixture: "dc-platform-annotation-tailor",
   192  			templateFixture: "dc-template",
   193  			expectedAction:  "Noop",
   194  		},
   195  		"Present in platform, changed in template": {
   196  			platformFixture: "dc-platform-annotation-applied",
   197  			templateFixture: "dc-template-changed",
   198  			expectedAction:  "Update",
   199  		},
   200  	}
   201  
   202  	for name, tc := range tests {
   203  		t.Run(name, func(t *testing.T) {
   204  			platformItem := getPlatformItem(t, "item-applied-config/"+tc.platformFixture+".yml")
   205  			templateItem := getTemplateItem(t, "item-applied-config/"+tc.templateFixture+".yml")
   206  			changes, err := calculateChanges(templateItem, platformItem, []string{}, true)
   207  			if err != nil {
   208  				t.Fatal(err)
   209  			}
   210  			if len(changes) != 1 {
   211  				t.Fatalf("Expected 1 change, got: %d", len(changes))
   212  			}
   213  			actualChange := changes[0]
   214  			if actualChange.Action != tc.expectedAction {
   215  				t.Fatalf("Expected change action to be: %s, got: %s. Diff:\n%s", tc.expectedAction, actualChange.Action, actualChange.Diff(true))
   216  			}
   217  		})
   218  	}
   219  }
   220  
   221  func TestCalculateChangesOmittedFields(t *testing.T) {
   222  
   223  	tests := map[string]struct {
   224  		platformFixture        string
   225  		templateFixture        string
   226  		expectedAction         string
   227  		expectedDiffGoldenFile string
   228  	}{
   229  		"Rolebinding with legacy fields": {
   230  			platformFixture:        "rolebinding-platform",
   231  			templateFixture:        "rolebinding-template",
   232  			expectedAction:         "Update",
   233  			expectedDiffGoldenFile: "rolebinding-changed",
   234  		},
   235  	}
   236  
   237  	for name, tc := range tests {
   238  		t.Run(name, func(t *testing.T) {
   239  			platformItem := getPlatformItem(t, "item-omitted-fields/"+tc.platformFixture+".yml")
   240  			templateItem := getTemplateItem(t, "item-omitted-fields/"+tc.templateFixture+".yml")
   241  			changes, err := calculateChanges(templateItem, platformItem, []string{}, true)
   242  			if err != nil {
   243  				t.Fatal(err)
   244  			}
   245  			if len(changes) != 1 {
   246  				t.Fatalf("Expected 1 change, got: %d", len(changes))
   247  			}
   248  			actualChange := changes[0]
   249  			if actualChange.Action != tc.expectedAction {
   250  				t.Fatalf("Expected change action to be: %s, got: %s", tc.expectedAction, actualChange.Action)
   251  			}
   252  			if len(tc.expectedDiffGoldenFile) > 0 {
   253  				want := strings.TrimSpace(getGoldenDiff(t, "item-omitted-fields", tc.expectedDiffGoldenFile+".txt"))
   254  				got := strings.TrimSpace(actualChange.Diff(true))
   255  				if diff := cmp.Diff(want, got); diff != "" {
   256  					t.Errorf("Change diff mismatch (-want +got):\n%s", diff)
   257  				}
   258  			}
   259  		})
   260  	}
   261  }
   262  
   263  func TestEmptyValuesDoNotCauseDrift(t *testing.T) {
   264  
   265  	tests := map[string]struct {
   266  		platformFixture string
   267  		templateFixture string
   268  		expectedAction  string
   269  	}{
   270  		"Field not defined in template": {
   271  			platformFixture: "bc-platform-defaulted.yml",
   272  			templateFixture: "bc-template-defaulted.yml",
   273  			expectedAction:  "Noop",
   274  		},
   275  		"Field not set in platform, and empty in template": {
   276  			platformFixture: "bc-platform-missing-env.yml",
   277  			templateFixture: "bc-template-empty-env.yml",
   278  			expectedAction:  "Noop",
   279  		},
   280  	}
   281  
   282  	for name, tc := range tests {
   283  		t.Run(name, func(t *testing.T) {
   284  			platformItem := getPlatformItem(t, "empty-values/"+tc.platformFixture)
   285  			templateItem := getTemplateItem(t, "empty-values/"+tc.templateFixture)
   286  			changes, err := calculateChanges(templateItem, platformItem, []string{}, true)
   287  			if err != nil {
   288  				t.Fatal(err)
   289  			}
   290  			if len(changes) != 1 {
   291  				t.Fatalf("Expected 1 change, got: %d", len(changes))
   292  			}
   293  			actualChange := changes[0]
   294  			if actualChange.Action != tc.expectedAction {
   295  				t.Fatalf("Expected change action to be: %s, got: %s. Diff was: %s", tc.expectedAction, actualChange.Action, actualChange.Diff(false))
   296  			}
   297  		})
   298  	}
   299  }
   300  
   301  func TestAddCreateOrder(t *testing.T) {
   302  	cs := fillChangeset("Create")
   303  	if cs.Create[0].Kind != "ServiceAccount" {
   304  		t.Errorf("SA needs to be created before PVC")
   305  	}
   306  	if cs.Create[1].Kind != "PersistentVolumeClaim" {
   307  		t.Errorf("PVC needs to be created before DC")
   308  	}
   309  }
   310  
   311  func TestAddUpdateOrder(t *testing.T) {
   312  	cs := fillChangeset("Update")
   313  	if cs.Update[0].Kind != "ServiceAccount" {
   314  		t.Errorf("SA needs to be created before PVC")
   315  	}
   316  	if cs.Update[1].Kind != "PersistentVolumeClaim" {
   317  		t.Errorf("PVC needs to be updated before DC")
   318  	}
   319  }
   320  
   321  func TestAddDeleteOrder(t *testing.T) {
   322  	cs := fillChangeset("Delete")
   323  	if cs.Delete[0].Kind != "DeploymentConfig" {
   324  		t.Errorf("DC needs to be deleted before PVC")
   325  	}
   326  	if cs.Delete[1].Kind != "PersistentVolumeClaim" {
   327  		t.Errorf("PVC needs to be deleted before SA")
   328  	}
   329  }
   330  
   331  func fillChangeset(action string) *Changeset {
   332  	cs := &Changeset{}
   333  	cDC := &Change{
   334  		Action: action,
   335  		Kind:   "DeploymentConfig",
   336  	}
   337  	cPVC := &Change{
   338  		Action: action,
   339  		Kind:   "PersistentVolumeClaim",
   340  	}
   341  	cSA := &Change{
   342  		Action: action,
   343  		Kind:   "ServiceAccount",
   344  	}
   345  	cs.Add(cPVC, cDC, cSA)
   346  	return cs
   347  }
   348  
   349  func TestConfigNoop(t *testing.T) {
   350  
   351  	templateInput := []byte(
   352  		`kind: List
   353  metadata: {}
   354  apiVersion: v1
   355  items:
   356  - apiVersion: v1
   357    kind: PersistentVolumeClaim
   358    metadata:
   359      labels:
   360        template: foo-template
   361      name: foo
   362    spec:
   363      accessModes:
   364      - ReadWriteOnce
   365      resources:
   366        requests:
   367          storage: 5Gi
   368      storageClassName: gp2
   369    status: {}`)
   370  
   371  	platformInput := []byte(
   372  		`kind: List
   373  metadata: {}
   374  apiVersion: v1
   375  items:
   376  - apiVersion: v1
   377    kind: PersistentVolumeClaim
   378    metadata:
   379      annotations:
   380        pv.kubernetes.io/bind-completed: "yes"
   381        pv.kubernetes.io/bound-by-controller: "yes"
   382        volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
   383      labels:
   384        template: foo-template
   385      name: foo
   386    spec:
   387      accessModes:
   388      - ReadWriteOnce
   389      resources:
   390        requests:
   391          storage: 5Gi
   392      storageClassName: gp2
   393      volumeName: pvc-2150713e-3e20-11e8-aa60-0aad3152d0e6
   394    status: {}`)
   395  
   396  	filter := &ResourceFilter{
   397  		Kinds: []string{"PersistentVolumeClaim"},
   398  	}
   399  	changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{})
   400  	if !changeset.Blank() {
   401  		t.Fatalf("Changeset is not blank!")
   402  	}
   403  }
   404  
   405  func TestConfigUpdate(t *testing.T) {
   406  
   407  	templateInput := []byte(
   408  		`kind: List
   409  metadata: {}
   410  apiVersion: v1
   411  items:
   412  - apiVersion: v1
   413    kind: PersistentVolumeClaim
   414    metadata:
   415      name: foo
   416      labels:
   417        app: foo
   418    spec:
   419      accessModes:
   420      - ReadWriteOnce
   421      resources:
   422        requests:
   423          storage: 5Gi
   424      storageClassName: gp2
   425    status: {}`)
   426  
   427  	platformInput := []byte(
   428  		`kind: List
   429  metadata: {}
   430  apiVersion: v1
   431  items:
   432  - apiVersion: v1
   433    kind: PersistentVolumeClaim
   434    metadata:
   435      name: foo
   436      annotations:
   437        kubectl.kubernetes.io/last-applied-configuration: >
   438          {"apiVersion":"1"}
   439    spec:
   440      accessModes:
   441      - ReadWriteOnce
   442      resources:
   443        requests:
   444          storage: 5Gi
   445      storageClassName: gp2
   446    status: {}`)
   447  
   448  	filter := &ResourceFilter{
   449  		Kinds: []string{"PersistentVolumeClaim"},
   450  	}
   451  	changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{})
   452  	if len(changeset.Update) != 1 {
   453  		t.Errorf("Changeset.Update has %d items instead of 1", len(changeset.Update))
   454  	}
   455  }
   456  
   457  func TestConfigPreservePaths(t *testing.T) {
   458  	templateInput := []byte(
   459  		`kind: List
   460  apiVersion: v1
   461  items:
   462  - apiVersion: v1
   463    kind: BuildConfig
   464    metadata:
   465      name: foo
   466    spec:
   467      failedBuildsHistoryLimit: 5
   468      output:
   469        to:
   470          kind: ImageStreamTag
   471          name: foo:latest
   472      postCommit: {}
   473      resources: {}
   474      runPolicy: Serial
   475      source:
   476        binary: {}
   477        type: Binary
   478      strategy:
   479        dockerStrategy: {}
   480        type: Docker
   481      successfulBuildsHistoryLimit: 5
   482      triggers:
   483      - generic:
   484          secret: password
   485        type: Generic`)
   486  
   487  	platformInput := []byte(
   488  		`kind: List
   489  apiVersion: v1
   490  items:
   491  - apiVersion: v1
   492    kind: BuildConfig
   493    metadata:
   494      name: foo
   495    spec:
   496      failedBuildsHistoryLimit: 5
   497      output:
   498        to:
   499          kind: ImageStreamTag
   500          name: foo:abcdef
   501        imageLabels:
   502        - name: bar
   503          value: baz
   504      postCommit: {}
   505      resources: {}
   506      runPolicy: Serial
   507      source:
   508        binary: {}
   509        type: Binary
   510      strategy:
   511        dockerStrategy: {}
   512        type: Docker
   513      successfulBuildsHistoryLimit: 5
   514      triggers:
   515      - generic:
   516          secret: password
   517        type: Generic`)
   518  
   519  	filter := &ResourceFilter{
   520  		Kinds: []string{"BuildConfig"},
   521  	}
   522  	changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{"bc:/spec/output/to/name", "bc:/spec/output/imageLabels"})
   523  	actualUpdates := len(changeset.Update)
   524  	expectedUpdates := 0
   525  	if actualUpdates != expectedUpdates {
   526  		t.Errorf("Changeset.Update has %d items instead of %d", actualUpdates, expectedUpdates)
   527  	}
   528  }
   529  
   530  func TestConfigCreation(t *testing.T) {
   531  	templateInput := []byte(
   532  		`kind: List
   533  metadata: {}
   534  apiVersion: v1
   535  items:
   536  - apiVersion: v1
   537    kind: PersistentVolumeClaim
   538    metadata:
   539      name: foo
   540    spec:
   541      accessModes:
   542      - ReadWriteOnce
   543      resources:
   544        requests:
   545          storage: 5Gi
   546      storageClassName: gp2
   547    status: {}`)
   548  
   549  	platformInput := []byte(
   550  		`kind: List
   551  metadata: {}
   552  apiVersion: v1
   553  items:
   554  - apiVersion: v1
   555    kind: PersistentVolumeClaim
   556    metadata:
   557      name: bar
   558    spec:
   559      accessModes:
   560      - ReadWriteOnce
   561      resources:
   562        requests:
   563          storage: 5Gi
   564      storageClassName: gp2
   565    status: {}`)
   566  
   567  	filter := &ResourceFilter{
   568  		Kinds: []string{"PersistentVolumeClaim"},
   569  	}
   570  	changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{})
   571  	if len(changeset.Create) != 1 {
   572  		t.Errorf("Changeset.Create is blank but should not be")
   573  	}
   574  }
   575  
   576  func TestConfigDeletion(t *testing.T) {
   577  
   578  	templateInput := []byte{}
   579  
   580  	platformInput := []byte(
   581  		`kind: List
   582  metadata: {}
   583  apiVersion: v1
   584  items:
   585  - apiVersion: v1
   586    kind: PersistentVolumeClaim
   587    metadata:
   588      name: foo
   589    spec:
   590      accessModes:
   591      - ReadWriteOnce
   592      resources:
   593        requests:
   594          storage: 5Gi
   595      storageClassName: gp2
   596    status: {}`)
   597  
   598  	filter := &ResourceFilter{
   599  		Kinds: []string{"PersistentVolumeClaim"},
   600  	}
   601  	changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{})
   602  	if len(changeset.Delete) != 1 {
   603  		t.Errorf("Changeset.Delete is blank but should not be")
   604  	}
   605  }
   606  
   607  func TestCalculateChangesEqual(t *testing.T) {
   608  	currentItem := getItem(t, getBuildConfig(), "platform")
   609  	desiredItem := getItem(t, getBuildConfig(), "template")
   610  	_, err := calculateChanges(desiredItem, currentItem, []string{}, true)
   611  	if err != nil {
   612  		t.Errorf(err.Error())
   613  	}
   614  }
   615  
   616  func TestCalculateChangesImmutableFields(t *testing.T) {
   617  	platformItem := getItem(t, getRoute([]byte("old.com")), "platform")
   618  
   619  	unchangedTemplateItem := getItem(t, getRoute([]byte("old.com")), "template")
   620  	changes, err := calculateChanges(unchangedTemplateItem, platformItem, []string{}, true)
   621  	if err != nil {
   622  		t.Errorf(err.Error())
   623  	}
   624  	if len(changes) > 1 || changes[0].Action != "Noop" {
   625  		t.Errorf("Platform and template should be in sync, got %d change(s): %v", len(changes), changes[0])
   626  	}
   627  
   628  	changedTemplateItem := getItem(t, getRoute([]byte("new.com")), "template")
   629  	changes, err = calculateChanges(changedTemplateItem, platformItem, []string{}, true)
   630  	if err != nil {
   631  		t.Errorf(err.Error())
   632  	}
   633  	if len(changes) == 0 {
   634  		t.Errorf("Platform and template should have drift.")
   635  	}
   636  }
   637  
   638  func getChangeset(t *testing.T, filter *ResourceFilter, platformInput, templateInput []byte, upsertOnly bool, allowRecreate bool, preservePaths []string) *Changeset {
   639  	platformBasedList, err := NewPlatformBasedResourceList(filter, platformInput)
   640  	if err != nil {
   641  		t.Error("Could not create platform based list:", err)
   642  	}
   643  	templateBasedList, err := NewTemplateBasedResourceList(filter, templateInput)
   644  	if err != nil {
   645  		t.Error("Could not create template based list:", err)
   646  	}
   647  	changeset, err := NewChangeset(platformBasedList, templateBasedList, upsertOnly, allowRecreate, preservePaths)
   648  	if err != nil {
   649  		t.Error("Could not create changeset:", err)
   650  	}
   651  	return changeset
   652  }
   653  
   654  func getGoldenDiff(t *testing.T, folder string, filename string) string {
   655  	b := helper.ReadGoldenFile(t, folder+"/"+filename)
   656  	return string(b)
   657  }