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 }