github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/kustomize/kustomizer_test.go (about)

     1  package kustomize
     2  
     3  import (
     4  	"context"
     5  	"path"
     6  	"testing"
     7  
     8  	"github.com/golang/mock/gomock"
     9  	"github.com/spf13/afero"
    10  	"github.com/stretchr/testify/require"
    11  	"sigs.k8s.io/kustomize/pkg/gvk"
    12  	"sigs.k8s.io/kustomize/pkg/patch"
    13  	"sigs.k8s.io/kustomize/pkg/types"
    14  
    15  	"github.com/replicatedhq/ship/pkg/api"
    16  	"github.com/replicatedhq/ship/pkg/constants"
    17  	"github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes"
    18  	"github.com/replicatedhq/ship/pkg/state"
    19  	daemon2 "github.com/replicatedhq/ship/pkg/test-mocks/daemon"
    20  	state2 "github.com/replicatedhq/ship/pkg/test-mocks/state"
    21  	"github.com/replicatedhq/ship/pkg/testing/logger"
    22  )
    23  
    24  const minimalValidYaml = `
    25  kind: Deployment
    26  metadata:
    27    name: myDeployment
    28  `
    29  
    30  func Test_kustomizer_writePatches(t *testing.T) {
    31  	destDir := path.Join("overlays", "ship")
    32  
    33  	type args struct {
    34  		shipOverlay state.Overlay
    35  		destDir     string
    36  	}
    37  	tests := []struct {
    38  		name        string
    39  		args        args
    40  		expectFiles map[string]string
    41  		want        []patch.StrategicMerge
    42  		wantErr     bool
    43  	}{
    44  		{
    45  			name: "No patches in state",
    46  			args: args{
    47  				shipOverlay: state.Overlay{
    48  					Patches: map[string]string{},
    49  				},
    50  				destDir: destDir,
    51  			},
    52  			expectFiles: map[string]string{},
    53  			want:        nil,
    54  		},
    55  		{
    56  			name: "Patches in state",
    57  			args: args{
    58  				shipOverlay: state.Overlay{
    59  					Patches: map[string]string{
    60  						"a.yaml":         "---",
    61  						"/folder/b.yaml": "---",
    62  					},
    63  				},
    64  				destDir: destDir,
    65  			},
    66  			expectFiles: map[string]string{
    67  				"a.yaml":        "---",
    68  				"folder/b.yaml": "---",
    69  			},
    70  			want: []patch.StrategicMerge{"a.yaml", "folder/b.yaml"},
    71  		},
    72  	}
    73  	for _, tt := range tests {
    74  		t.Run(tt.name, func(t *testing.T) {
    75  			req := require.New(t)
    76  			mc := gomock.NewController(t)
    77  			testLogger := &logger.TestLogger{T: t}
    78  			mockDaemon := daemon2.NewMockDaemon(mc)
    79  			mockState := state2.NewMockManager(mc)
    80  
    81  			// need a real FS because afero.Rename on a memMapFs doesn't copy directories recursively
    82  			fs := afero.Afero{Fs: afero.NewOsFs()}
    83  			tmpdir, err := fs.TempDir("./", tt.name)
    84  			req.NoError(err)
    85  			defer fs.RemoveAll(tmpdir) // nolint: errcheck
    86  
    87  			mockFs := afero.Afero{Fs: afero.NewBasePathFs(afero.NewOsFs(), tmpdir)}
    88  			// its chrooted to a temp dir, but this needs to exist
    89  			err = mockFs.MkdirAll(".ship/tmp/", 0755)
    90  			req.NoError(err)
    91  			l := &daemonkustomizer{
    92  				Kustomizer: Kustomizer{
    93  					Logger: testLogger,
    94  					State:  mockState,
    95  					FS:     mockFs,
    96  				},
    97  				Daemon: mockDaemon,
    98  			}
    99  
   100  			got, err := l.writePatches(mockFs, tt.args.shipOverlay, tt.args.destDir)
   101  			if (err != nil) != tt.wantErr {
   102  				t.Errorf("kustomizer.writePatches() error = %v, wantErr %v", err, tt.wantErr)
   103  				return
   104  			}
   105  
   106  			for _, filename := range tt.want {
   107  				req.Contains(got, filename)
   108  			}
   109  
   110  			for file, contents := range tt.expectFiles {
   111  				fileBytes, err := l.FS.ReadFile(path.Join(destDir, file))
   112  				if err != nil {
   113  					t.Errorf("expected file at %v, received error instead: %v", file, err)
   114  				}
   115  				req.Equal(contents, string(fileBytes))
   116  			}
   117  		})
   118  	}
   119  }
   120  
   121  func Test_kustomizer_writeOverlay(t *testing.T) {
   122  	mockStep := api.Kustomize{
   123  		Base:    constants.KustomizeBasePath,
   124  		Overlay: path.Join("overlays", "ship"),
   125  	}
   126  
   127  	tests := []struct {
   128  		name                  string
   129  		relativePatchPaths    []patch.StrategicMerge
   130  		existingKustomization types.Kustomization
   131  		expectFile            string
   132  		wantErr               bool
   133  	}{
   134  		{
   135  			name:               "No patches",
   136  			relativePatchPaths: []patch.StrategicMerge{},
   137  			expectFile: `kind: ""
   138  apiversion: ""
   139  bases:
   140  - ../defaults
   141  `,
   142  		},
   143  		{
   144  			name:               "Patches provided",
   145  			relativePatchPaths: []patch.StrategicMerge{"a.yaml", "b.yaml", "c.yaml"},
   146  			expectFile: `kind: ""
   147  apiversion: ""
   148  patchesStrategicMerge:
   149  - a.yaml
   150  - b.yaml
   151  - c.yaml
   152  bases:
   153  - ../defaults
   154  `,
   155  		},
   156  		{
   157  			name:               "No patches but existing kustomization",
   158  			relativePatchPaths: []patch.StrategicMerge{},
   159  			existingKustomization: types.Kustomization{
   160  				PatchesJson6902: []patch.Json6902{
   161  					{
   162  						Path: "abc.json",
   163  						Target: &patch.Target{
   164  							Gvk: gvk.Gvk{
   165  								Group:   "groupa",
   166  								Version: "versionb",
   167  								Kind:    "kindc",
   168  							},
   169  							Namespace: "nsd",
   170  							Name:      "namee",
   171  						},
   172  					},
   173  				},
   174  			},
   175  			expectFile: `kind: ""
   176  apiversion: ""
   177  patchesJson6902:
   178  - target:
   179      group: groupa
   180      version: versionb
   181      kind: kindc
   182      namespace: nsd
   183      name: namee
   184    path: abc.json
   185  bases:
   186  - ../defaults
   187  `,
   188  		},
   189  	}
   190  	for _, tt := range tests {
   191  		t.Run(tt.name, func(t *testing.T) {
   192  			req := require.New(t)
   193  			mc := gomock.NewController(t)
   194  			testLogger := &logger.TestLogger{T: t}
   195  			mockDaemon := daemon2.NewMockDaemon(mc)
   196  			mockState := state2.NewMockManager(mc)
   197  			mockFs := afero.Afero{Fs: afero.NewMemMapFs()}
   198  
   199  			l := &daemonkustomizer{
   200  				Kustomizer: Kustomizer{
   201  					Logger: testLogger,
   202  					State:  mockState,
   203  					FS:     mockFs,
   204  				},
   205  				Daemon: mockDaemon,
   206  			}
   207  			if err := l.writeOverlay(mockStep, tt.relativePatchPaths, nil, tt.existingKustomization); (err != nil) != tt.wantErr {
   208  				t.Errorf("kustomizer.writeOverlay() error = %v, wantErr %v", err, tt.wantErr)
   209  			}
   210  
   211  			overlayPathDest := path.Join(mockStep.OverlayPath(), "kustomization.yaml")
   212  			fileBytes, err := l.FS.ReadFile(overlayPathDest)
   213  			if err != nil {
   214  				t.Errorf("expected file at %v, received error instead: %v", overlayPathDest, err)
   215  			}
   216  			req.Equal(tt.expectFile, string(fileBytes))
   217  		})
   218  	}
   219  }
   220  
   221  func Test_kustomizer_writeBase(t *testing.T) {
   222  	mockStep := api.Kustomize{
   223  		Base:    constants.KustomizeBasePath,
   224  		Overlay: path.Join("overlays", "ship"),
   225  	}
   226  
   227  	type fields struct {
   228  		GetFS func() (afero.Afero, error)
   229  	}
   230  	tests := []struct {
   231  		name          string
   232  		fields        fields
   233  		expectFile    string
   234  		wantErr       bool
   235  		excludedBases []string
   236  	}{
   237  		{
   238  			name: "No base files",
   239  			fields: fields{
   240  				GetFS: func() (afero.Afero, error) {
   241  					fs := afero.Afero{Fs: afero.NewMemMapFs()}
   242  					err := fs.Mkdir(constants.KustomizeBasePath, 0777)
   243  					if err != nil {
   244  						return afero.Afero{}, err
   245  					}
   246  					return fs, nil
   247  				},
   248  			},
   249  			wantErr: true,
   250  		},
   251  		{
   252  			name: "Flat base files",
   253  			fields: fields{
   254  				GetFS: func() (afero.Afero, error) {
   255  					fs := afero.Afero{Fs: afero.NewMemMapFs()}
   256  					if err := fs.Mkdir(constants.KustomizeBasePath, 0777); err != nil {
   257  						return afero.Afero{}, err
   258  					}
   259  
   260  					files := []string{"a.yaml", "b.yaml", "c.yaml"}
   261  					for _, file := range files {
   262  						if err := fs.WriteFile(
   263  							path.Join(constants.KustomizeBasePath, file),
   264  							[]byte(minimalValidYaml),
   265  							0777,
   266  						); err != nil {
   267  							return afero.Afero{}, err
   268  						}
   269  					}
   270  
   271  					return fs, nil
   272  				},
   273  			},
   274  			expectFile: `kind: ""
   275  apiversion: ""
   276  resources:
   277  - a.yaml
   278  - b.yaml
   279  - c.yaml
   280  `,
   281  		},
   282  		{
   283  			name: "Base files with nested chart",
   284  			fields: fields{
   285  				GetFS: func() (afero.Afero, error) {
   286  					fs := afero.Afero{Fs: afero.NewMemMapFs()}
   287  					nestedChartPath := path.Join(
   288  						constants.KustomizeBasePath,
   289  						"charts/kube-stats-metrics/templates",
   290  					)
   291  					if err := fs.MkdirAll(nestedChartPath, 0777); err != nil {
   292  						return afero.Afero{}, err
   293  					}
   294  
   295  					files := []string{
   296  						"deployment.yaml",
   297  						"clusterrole.yaml",
   298  						"charts/kube-stats-metrics/templates/deployment.yaml",
   299  					}
   300  					for _, file := range files {
   301  						if err := fs.WriteFile(
   302  							path.Join(constants.KustomizeBasePath, file),
   303  							[]byte(minimalValidYaml),
   304  							0777,
   305  						); err != nil {
   306  							return afero.Afero{}, err
   307  						}
   308  					}
   309  
   310  					return fs, nil
   311  				},
   312  			},
   313  			expectFile: `kind: ""
   314  apiversion: ""
   315  resources:
   316  - charts/kube-stats-metrics/templates/deployment.yaml
   317  - clusterrole.yaml
   318  - deployment.yaml
   319  `,
   320  		},
   321  		{
   322  			name: "Base files with nested and excluded chart",
   323  			fields: fields{
   324  				GetFS: func() (afero.Afero, error) {
   325  					fs := afero.Afero{Fs: afero.NewMemMapFs()}
   326  					nestedChartPath := path.Join(
   327  						constants.KustomizeBasePath,
   328  						"charts/kube-stats-metrics/templates",
   329  					)
   330  					if err := fs.MkdirAll(nestedChartPath, 0777); err != nil {
   331  						return afero.Afero{}, err
   332  					}
   333  
   334  					files := []string{
   335  						"deployment.yaml",
   336  						"clusterrole.yaml",
   337  						"charts/kube-stats-metrics/templates/deployment.yaml",
   338  					}
   339  					for _, file := range files {
   340  						if err := fs.WriteFile(
   341  							path.Join(constants.KustomizeBasePath, file),
   342  							[]byte(minimalValidYaml),
   343  							0777,
   344  						); err != nil {
   345  							return afero.Afero{}, err
   346  						}
   347  					}
   348  
   349  					return fs, nil
   350  				},
   351  			},
   352  			expectFile: `kind: ""
   353  apiversion: ""
   354  resources:
   355  - charts/kube-stats-metrics/templates/deployment.yaml
   356  - deployment.yaml
   357  `,
   358  			excludedBases: []string{"/clusterrole.yaml"},
   359  		},
   360  	}
   361  	for _, tt := range tests {
   362  		t.Run(tt.name, func(t *testing.T) {
   363  			req := require.New(t)
   364  			mc := gomock.NewController(t)
   365  			testLogger := &logger.TestLogger{T: t}
   366  			mockDaemon := daemon2.NewMockDaemon(mc)
   367  			mockState := state2.NewMockManager(mc)
   368  
   369  			mockState.EXPECT().CachedState().Return(state.State{
   370  				V1: &state.V1{
   371  					Kustomize: &state.Kustomize{
   372  						Overlays: map[string]state.Overlay{
   373  							"ship": state.Overlay{
   374  								ExcludedBases: tt.excludedBases,
   375  							},
   376  						},
   377  					},
   378  				},
   379  			}, nil).AnyTimes()
   380  
   381  			fs, err := tt.fields.GetFS()
   382  			req.NoError(err)
   383  
   384  			l := &daemonkustomizer{
   385  				Kustomizer: Kustomizer{
   386  					Logger: testLogger,
   387  					State:  mockState,
   388  					FS:     fs,
   389  				},
   390  				Daemon: mockDaemon,
   391  			}
   392  
   393  			if err := l.writeBase(mockStep.Base); (err != nil) != tt.wantErr {
   394  				t.Errorf("kustomizer.writeBase() error = %v, wantErr %v", err, tt.wantErr)
   395  			} else if err == nil {
   396  				basePathDest := path.Join(mockStep.Base, "kustomization.yaml")
   397  				fileBytes, err := l.FS.ReadFile(basePathDest)
   398  				if err != nil {
   399  					t.Errorf("expected file at %v, received error instead: %v", basePathDest, err)
   400  				}
   401  				req.Equal(tt.expectFile, string(fileBytes))
   402  			}
   403  		})
   404  	}
   405  }
   406  
   407  func TestKustomizer(t *testing.T) {
   408  	tests := []struct {
   409  		name        string
   410  		kustomize   *state.Kustomize
   411  		expectFiles map[string]string
   412  	}{
   413  		{
   414  			name:      "no files",
   415  			kustomize: nil,
   416  			expectFiles: map[string]string{
   417  				"overlays/ship/kustomization.yaml": `kind: ""
   418  apiversion: ""
   419  bases:
   420  - ../defaults
   421  `,
   422  				"base/kustomization.yaml": `kind: ""
   423  apiversion: ""
   424  resources:
   425  - deployment.yaml
   426  `,
   427  			},
   428  		},
   429  		{
   430  			name: "one file",
   431  			kustomize: &state.Kustomize{
   432  				Overlays: map[string]state.Overlay{
   433  					"ship": {
   434  						Patches: map[string]string{
   435  							"/deployment.yaml": `---
   436  metadata:
   437    name: my-deploy
   438  spec:
   439    replicas: 100`,
   440  						},
   441  					},
   442  				},
   443  			},
   444  			expectFiles: map[string]string{
   445  				"overlays/ship/deployment.yaml": `---
   446  metadata:
   447    name: my-deploy
   448  spec:
   449    replicas: 100`,
   450  
   451  				"overlays/ship/kustomization.yaml": `kind: ""
   452  apiversion: ""
   453  patchesStrategicMerge:
   454  - deployment.yaml
   455  bases:
   456  - ../defaults
   457  `,
   458  				"base/kustomization.yaml": `kind: ""
   459  apiversion: ""
   460  resources:
   461  - deployment.yaml
   462  `,
   463  			},
   464  		},
   465  		{
   466  			name: "adding a resource",
   467  			kustomize: &state.Kustomize{
   468  				Overlays: map[string]state.Overlay{
   469  					"ship": {
   470  						Resources: map[string]string{
   471  							"/limitrange.yaml": `---
   472  apiVersion: v1
   473  kind: LimitRange
   474  metadata:
   475    name: mem-limit-range
   476  spec:
   477    limits:
   478    - default:
   479        memory: 512Mi
   480      defaultRequest:
   481        memory: 256Mi
   482      type: Container`,
   483  						},
   484  					},
   485  				},
   486  			},
   487  			expectFiles: map[string]string{
   488  				"overlays/ship/limitrange.yaml": `---
   489  apiVersion: v1
   490  kind: LimitRange
   491  metadata:
   492    name: mem-limit-range
   493  spec:
   494    limits:
   495    - default:
   496        memory: 512Mi
   497      defaultRequest:
   498        memory: 256Mi
   499      type: Container`,
   500  
   501  				"overlays/ship/kustomization.yaml": `kind: ""
   502  apiversion: ""
   503  resources:
   504  - limitrange.yaml
   505  bases:
   506  - ../defaults
   507  `,
   508  				"base/kustomization.yaml": `kind: ""
   509  apiversion: ""
   510  resources:
   511  - deployment.yaml
   512  `,
   513  			},
   514  		},
   515  	}
   516  	for _, test := range tests {
   517  		t.Run(test.name, func(t *testing.T) {
   518  			req := require.New(t)
   519  			mc := gomock.NewController(t)
   520  			testLogger := &logger.TestLogger{T: t}
   521  			mockDaemon := daemon2.NewMockDaemon(mc)
   522  			mockState := state2.NewMockManager(mc)
   523  
   524  			mockFS := afero.Afero{Fs: afero.NewMemMapFs()}
   525  			err := mockFS.Mkdir(constants.KustomizeBasePath, 0777)
   526  			req.NoError(err)
   527  
   528  			err = mockFS.WriteFile(
   529  				path.Join(constants.KustomizeBasePath, "deployment.yaml"),
   530  				[]byte(minimalValidYaml),
   531  				0666,
   532  			)
   533  			req.NoError(err)
   534  
   535  			saveChan := make(chan interface{})
   536  			close(saveChan)
   537  
   538  			ctx := context.Background()
   539  			release := api.Release{}
   540  
   541  			mockDaemon.EXPECT().EnsureStarted(ctx, &release)
   542  			mockDaemon.EXPECT().PushKustomizeStep(ctx, daemontypes.Kustomize{
   543  				BasePath: constants.KustomizeBasePath,
   544  			})
   545  			mockDaemon.EXPECT().KustomizeSavedChan().Return(saveChan)
   546  			mockState.EXPECT().CachedState().Return(state.State{V1: &state.V1{
   547  				Kustomize: test.kustomize,
   548  			}}, nil).Times(2)
   549  
   550  			k := &daemonkustomizer{
   551  				Kustomizer: Kustomizer{
   552  					Logger: testLogger,
   553  					FS:     mockFS,
   554  					State:  mockState,
   555  				},
   556  				Daemon: mockDaemon,
   557  			}
   558  
   559  			err = k.Execute(
   560  				ctx,
   561  				&release,
   562  				api.Kustomize{
   563  					Base:    constants.KustomizeBasePath,
   564  					Overlay: "overlays/ship",
   565  				},
   566  			)
   567  
   568  			for name, contents := range test.expectFiles {
   569  				actual, err := mockFS.ReadFile(name)
   570  				req.NoError(err, "read expected file %s", name)
   571  				req.Equal(contents, string(actual))
   572  			}
   573  
   574  			req.NoError(err)
   575  		})
   576  	}
   577  }