github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/runner/v1/runner_test.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package v1 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "io" 24 "testing" 25 26 "github.com/blang/semver" 27 "k8s.io/client-go/tools/clientcmd/api" 28 29 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/access" 30 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build" 31 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/cluster" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy" 35 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/helm" 36 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kubectl" 37 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kustomize" 38 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon" 39 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph" 40 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/log" 41 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform" 42 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner" 43 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext" 44 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/defaults" 45 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 46 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/status" 47 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync" 48 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/tag" 49 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/test" 50 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 51 "github.com/GoogleContainerTools/skaffold/testutil" 52 ) 53 54 type Actions struct { 55 Built []string 56 Synced []string 57 Tested []string 58 Deployed []string 59 } 60 61 type TestBench struct { 62 buildErrors []error 63 syncErrors []error 64 testErrors []error 65 deployErrors []error 66 namespaces []string 67 userIntents []func(*runner.Intents) 68 intents *runner.Intents 69 intentTrigger bool 70 71 devLoop func(context.Context, io.Writer, func() error) error 72 firstMonitor func(bool) error 73 cycles int 74 currentCycle int 75 currentActions Actions 76 actions []Actions 77 tag int 78 } 79 80 func NewTestBench() *TestBench { 81 return &TestBench{} 82 } 83 84 func (t *TestBench) WithBuildErrors(buildErrors []error) *TestBench { 85 t.buildErrors = buildErrors 86 return t 87 } 88 89 func (t *TestBench) WithSyncErrors(syncErrors []error) *TestBench { 90 t.syncErrors = syncErrors 91 return t 92 } 93 94 func (t *TestBench) WithDeployErrors(deployErrors []error) *TestBench { 95 t.deployErrors = deployErrors 96 return t 97 } 98 99 func (t *TestBench) WithDeployNamespaces(ns []string) *TestBench { 100 t.namespaces = ns 101 return t 102 } 103 104 func (t *TestBench) WithTestErrors(testErrors []error) *TestBench { 105 t.testErrors = testErrors 106 return t 107 } 108 109 func (t *TestBench) GetAccessor() access.Accessor { 110 return &access.NoopAccessor{} 111 } 112 113 func (t *TestBench) GetDebugger() debug.Debugger { 114 return &debug.NoopDebugger{} 115 } 116 117 func (t *TestBench) GetLogger() log.Logger { 118 return &log.NoopLogger{} 119 } 120 121 func (t *TestBench) GetStatusMonitor() status.Monitor { 122 return &status.NoopMonitor{} 123 } 124 125 func (t *TestBench) GetSyncer() sync.Syncer { 126 return t 127 } 128 129 func (t *TestBench) RegisterLocalImages(_ []graph.Artifact) {} 130 func (t *TestBench) TrackBuildArtifacts(_ []graph.Artifact) {} 131 132 func (t *TestBench) TestDependencies(context.Context, *latest.Artifact) ([]string, error) { 133 return nil, nil 134 } 135 func (t *TestBench) Dependencies() ([]string, error) { return nil, nil } 136 func (t *TestBench) Cleanup(ctx context.Context, out io.Writer, dryRun bool) error { return nil } 137 func (t *TestBench) Prune(ctx context.Context, out io.Writer) error { return nil } 138 139 func (t *TestBench) enterNewCycle() { 140 t.actions = append(t.actions, t.currentActions) 141 t.currentActions = Actions{} 142 } 143 144 func (t *TestBench) Build(_ context.Context, _ io.Writer, _ tag.ImageTags, _ platform.Resolver, artifacts []*latest.Artifact) ([]graph.Artifact, error) { 145 if len(t.buildErrors) > 0 { 146 err := t.buildErrors[0] 147 t.buildErrors = t.buildErrors[1:] 148 if err != nil { 149 return nil, err 150 } 151 } 152 153 t.tag++ 154 155 var builds []graph.Artifact 156 for _, artifact := range artifacts { 157 builds = append(builds, graph.Artifact{ 158 ImageName: artifact.ImageName, 159 Tag: fmt.Sprintf("%s:%d", artifact.ImageName, t.tag), 160 }) 161 } 162 163 t.currentActions.Built = findTags(builds) 164 return builds, nil 165 } 166 167 func (t *TestBench) Sync(_ context.Context, _ io.Writer, item *sync.Item) error { 168 if len(t.syncErrors) > 0 { 169 err := t.syncErrors[0] 170 t.syncErrors = t.syncErrors[1:] 171 if err != nil { 172 return err 173 } 174 } 175 176 t.currentActions.Synced = []string{item.Image} 177 return nil 178 } 179 180 func (t *TestBench) Test(_ context.Context, _ io.Writer, artifacts []graph.Artifact) error { 181 if len(t.testErrors) > 0 { 182 err := t.testErrors[0] 183 t.testErrors = t.testErrors[1:] 184 if err != nil { 185 return err 186 } 187 } 188 189 t.currentActions.Tested = findTags(artifacts) 190 return nil 191 } 192 193 func (t *TestBench) Deploy(_ context.Context, _ io.Writer, artifacts []graph.Artifact) error { 194 if len(t.deployErrors) > 0 { 195 err := t.deployErrors[0] 196 t.deployErrors = t.deployErrors[1:] 197 if err != nil { 198 return err 199 } 200 } 201 202 t.currentActions.Deployed = findTags(artifacts) 203 return nil 204 } 205 206 func (t *TestBench) Render(context.Context, io.Writer, []graph.Artifact, bool, string) error { 207 return nil 208 } 209 210 func (t *TestBench) Actions() []Actions { 211 return append(t.actions, t.currentActions) 212 } 213 214 func (t *TestBench) WatchForChanges(ctx context.Context, out io.Writer, doDev func() error) error { 215 // don't actually call the monitor here, because extra actions would be added 216 if err := t.firstMonitor(true); err != nil { 217 return err 218 } 219 220 t.intentTrigger = true 221 for _, intent := range t.userIntents { 222 intent(t.intents) 223 if err := t.devLoop(ctx, out, doDev); err != nil { 224 return err 225 } 226 } 227 228 t.intentTrigger = false 229 for i := 0; i < t.cycles; i++ { 230 t.enterNewCycle() 231 t.currentCycle = i 232 if err := t.devLoop(ctx, out, doDev); err != nil { 233 return err 234 } 235 } 236 return nil 237 } 238 239 func (t *TestBench) LogWatchToUser(_ io.Writer) {} 240 241 func findTags(artifacts []graph.Artifact) []string { 242 var tags []string 243 for _, artifact := range artifacts { 244 tags = append(tags, artifact.Tag) 245 } 246 return tags 247 } 248 249 func (r *SkaffoldRunner) WithMonitor(m filemon.Monitor) *SkaffoldRunner { 250 r.monitor = m 251 return r 252 } 253 254 type triggerState struct { 255 build bool 256 sync bool 257 deploy bool 258 } 259 260 func createRunner(t *testutil.T, testBench *TestBench, monitor filemon.Monitor, artifacts []*latest.Artifact, autoTriggers *triggerState) *SkaffoldRunner { 261 if autoTriggers == nil { 262 autoTriggers = &triggerState{true, true, true} 263 } 264 var tests []*latest.TestCase 265 for _, a := range artifacts { 266 tests = append(tests, &latest.TestCase{ 267 ImageName: a.ImageName, 268 }) 269 } 270 cfg := &latest.SkaffoldConfig{ 271 Pipeline: latest.Pipeline{ 272 Build: latest.BuildConfig{ 273 TagPolicy: latest.TagPolicy{ 274 // Use the fastest tagger 275 ShaTagger: &latest.ShaTagger{}, 276 }, 277 Artifacts: artifacts, 278 }, 279 Test: tests, 280 Deploy: latest.DeployConfig{StatusCheckDeadlineSeconds: 60}, 281 }, 282 } 283 defaults.Set(cfg) 284 defaults.SetDefaultDeployer(cfg) 285 runCtx := &runcontext.RunContext{ 286 Pipelines: runcontext.NewPipelines([]latest.Pipeline{cfg.Pipeline}), 287 Opts: config.SkaffoldOptions{ 288 Trigger: "polling", 289 WatchPollInterval: 100, 290 AutoBuild: autoTriggers.build, 291 AutoSync: autoTriggers.sync, 292 AutoDeploy: autoTriggers.deploy, 293 }, 294 } 295 runner, err := NewForConfig(context.Background(), runCtx) 296 t.CheckNoError(err) 297 298 // TODO(yuwenma):builder.builder looks weird. Avoid the nested struct. 299 runner.Builder.Builder = testBench 300 runner.tester = testBench 301 runner.deployer = testBench 302 runner.listener = testBench 303 runner.monitor = monitor 304 305 testBench.devLoop = func(ctx context.Context, out io.Writer, doDev func() error) error { 306 if err := monitor.Run(true); err != nil { 307 return err 308 } 309 return doDev() 310 } 311 312 testBench.firstMonitor = func(bool) error { 313 // default to noop so we don't add extra actions 314 return nil 315 } 316 317 return runner 318 } 319 320 func TestNewForConfig(t *testing.T) { 321 tests := []struct { 322 description string 323 pipeline latest.Pipeline 324 shouldErr bool 325 cacheArtifacts bool 326 expectedBuilder build.BuilderMux 327 expectedTester test.Tester 328 expectedDeployer deploy.Deployer 329 }{ 330 { 331 description: "local builder config", 332 pipeline: latest.Pipeline{ 333 Build: latest.BuildConfig{ 334 TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}}, 335 BuildType: latest.BuildType{ 336 LocalBuild: &latest.LocalBuild{}, 337 }, 338 }, 339 Deploy: latest.DeployConfig{ 340 DeployType: latest.DeployType{ 341 KubectlDeploy: &latest.KubectlDeploy{}, 342 }, 343 }, 344 }, 345 expectedTester: &test.FullTester{}, 346 expectedDeployer: &kubectl.Deployer{}, 347 }, 348 { 349 description: "gcb config", 350 pipeline: latest.Pipeline{ 351 Build: latest.BuildConfig{ 352 TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}}, 353 BuildType: latest.BuildType{ 354 GoogleCloudBuild: &latest.GoogleCloudBuild{}, 355 }, 356 }, 357 Deploy: latest.DeployConfig{ 358 DeployType: latest.DeployType{ 359 KubectlDeploy: &latest.KubectlDeploy{}, 360 }, 361 }, 362 }, 363 expectedTester: &test.FullTester{}, 364 expectedDeployer: &kubectl.Deployer{}, 365 }, 366 { 367 description: "cluster builder config", 368 pipeline: latest.Pipeline{ 369 Build: latest.BuildConfig{ 370 TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}}, 371 BuildType: latest.BuildType{ 372 Cluster: &latest.ClusterDetails{Timeout: "100s"}, 373 }, 374 }, 375 Deploy: latest.DeployConfig{ 376 DeployType: latest.DeployType{ 377 KubectlDeploy: &latest.KubectlDeploy{}, 378 }, 379 }, 380 }, 381 expectedTester: &test.FullTester{}, 382 expectedDeployer: &kubectl.Deployer{}, 383 }, 384 { 385 description: "bad tagger config", 386 pipeline: latest.Pipeline{ 387 Build: latest.BuildConfig{ 388 TagPolicy: latest.TagPolicy{}, 389 BuildType: latest.BuildType{ 390 LocalBuild: &latest.LocalBuild{}, 391 }, 392 }, 393 Deploy: latest.DeployConfig{ 394 DeployType: latest.DeployType{ 395 KubectlDeploy: &latest.KubectlDeploy{}, 396 }, 397 }, 398 }, 399 shouldErr: true, 400 }, 401 { 402 description: "unknown builder and tagger", 403 pipeline: latest.Pipeline{}, 404 shouldErr: true, 405 expectedTester: &test.FullTester{}, 406 expectedDeployer: &kubectl.Deployer{}, 407 }, 408 { 409 description: "no artifacts, cache", 410 pipeline: latest.Pipeline{ 411 Build: latest.BuildConfig{ 412 TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}}, 413 BuildType: latest.BuildType{ 414 LocalBuild: &latest.LocalBuild{}, 415 }, 416 }, 417 Deploy: latest.DeployConfig{ 418 DeployType: latest.DeployType{ 419 KubectlDeploy: &latest.KubectlDeploy{}, 420 }, 421 }, 422 }, 423 expectedTester: &test.FullTester{}, 424 expectedDeployer: &kubectl.Deployer{}, 425 cacheArtifacts: true, 426 }, 427 { 428 description: "transformableAllowList", 429 pipeline: latest.Pipeline{ 430 Build: latest.BuildConfig{ 431 TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}}, 432 BuildType: latest.BuildType{ 433 LocalBuild: &latest.LocalBuild{}, 434 }, 435 }, 436 Deploy: latest.DeployConfig{ 437 DeployType: latest.DeployType{ 438 KubectlDeploy: &latest.KubectlDeploy{}, 439 }, 440 }, 441 ResourceSelector: latest.ResourceSelectorConfig{ 442 Allow: []latest.ResourceFilter{ 443 { 444 GroupKind: "example.com/Application", 445 }, 446 }, 447 }, 448 }, 449 expectedTester: &test.FullTester{}, 450 expectedDeployer: &kubectl.Deployer{}, 451 }, 452 { 453 description: "multiple deployers", 454 pipeline: latest.Pipeline{ 455 Build: latest.BuildConfig{ 456 TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}}, 457 BuildType: latest.BuildType{ 458 LocalBuild: &latest.LocalBuild{}, 459 }, 460 }, 461 Deploy: latest.DeployConfig{ 462 DeployType: latest.DeployType{ 463 KubectlDeploy: &latest.KubectlDeploy{}, 464 KustomizeDeploy: &latest.KustomizeDeploy{}, 465 HelmDeploy: &latest.HelmDeploy{}, 466 }, 467 }, 468 }, 469 expectedTester: &test.FullTester{}, 470 expectedDeployer: deploy.NewDeployerMux([]deploy.Deployer{ 471 &helm.Deployer{}, 472 &kubectl.Deployer{}, 473 &kustomize.Deployer{}, 474 }, false), 475 }, 476 } 477 for _, test := range tests { 478 testutil.Run(t, test.description, func(t *testutil.T) { 479 t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"}) 480 t.Override(&cluster.FindMinikubeBinary, func(context.Context) (string, semver.Version, error) { 481 return "", semver.Version{}, errors.New("not found") 482 }) 483 t.Override(&util.DefaultExecCommand, testutil.CmdRunWithOutput( 484 "helm version --client", `version.BuildInfo{Version:"v3.0.0"}`). 485 AndRunWithOutput("kubectl version --client -ojson", "v1.5.6")) 486 487 runCtx := &runcontext.RunContext{ 488 Pipelines: runcontext.NewPipelines([]latest.Pipeline{test.pipeline}), 489 Opts: config.SkaffoldOptions{ 490 Trigger: "polling", 491 }, 492 } 493 // Test transformableAllowList 494 filters := runCtx.TransformAllowList() 495 if test.pipeline.ResourceSelector.Allow != nil { 496 t.CheckDeepEqual(test.pipeline.ResourceSelector.Allow, filters) 497 } else { 498 t.CheckEmpty(filters) 499 } 500 501 cfg, err := NewForConfig(context.Background(), runCtx) 502 t.CheckError(test.shouldErr, err) 503 if cfg != nil { 504 b, _t, d := runner.WithTimings(&test.expectedBuilder, test.expectedTester, test.expectedDeployer, test.cacheArtifacts) 505 if test.shouldErr { 506 t.CheckError(true, err) 507 } else { 508 t.CheckNoError(err) 509 t.CheckTypeEquality(b, cfg.Pruner.Builder) 510 t.CheckTypeEquality(_t, cfg.tester) 511 t.CheckTypeEquality(d, cfg.deployer) 512 } 513 } 514 }) 515 } 516 } 517 518 func TestTriggerCallbackAndIntents(t *testing.T) { 519 var tests = []struct { 520 description string 521 autoBuild bool 522 autoSync bool 523 autoDeploy bool 524 expectedBuildIntent bool 525 expectedSyncIntent bool 526 expectedDeployIntent bool 527 }{ 528 { 529 description: "default", 530 autoBuild: true, 531 autoSync: true, 532 autoDeploy: true, 533 expectedBuildIntent: true, 534 expectedSyncIntent: true, 535 expectedDeployIntent: true, 536 }, 537 { 538 description: "build trigger in api mode", 539 autoBuild: false, 540 autoSync: true, 541 autoDeploy: true, 542 expectedBuildIntent: false, 543 expectedSyncIntent: true, 544 expectedDeployIntent: true, 545 }, 546 { 547 description: "deploy trigger in api mode", 548 autoBuild: true, 549 autoSync: true, 550 autoDeploy: false, 551 expectedBuildIntent: true, 552 expectedSyncIntent: true, 553 expectedDeployIntent: false, 554 }, 555 { 556 description: "sync trigger in api mode", 557 autoBuild: true, 558 autoSync: false, 559 autoDeploy: true, 560 expectedBuildIntent: true, 561 expectedSyncIntent: false, 562 expectedDeployIntent: true, 563 }, 564 } 565 566 for _, test := range tests { 567 testutil.Run(t, test.description, func(t *testutil.T) { 568 opts := config.SkaffoldOptions{ 569 Trigger: "polling", 570 WatchPollInterval: 100, 571 AutoBuild: test.autoBuild, 572 AutoSync: test.autoSync, 573 AutoDeploy: test.autoDeploy, 574 } 575 pipeline := latest.Pipeline{ 576 Build: latest.BuildConfig{ 577 TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}}, 578 BuildType: latest.BuildType{ 579 LocalBuild: &latest.LocalBuild{}, 580 }, 581 }, 582 Deploy: latest.DeployConfig{ 583 DeployType: latest.DeployType{ 584 KubectlDeploy: &latest.KubectlDeploy{}, 585 }, 586 }, 587 } 588 r, _ := NewForConfig(context.Background(), &runcontext.RunContext{ 589 Opts: opts, 590 Pipelines: runcontext.NewPipelines([]latest.Pipeline{pipeline}), 591 }) 592 593 r.intents.ResetBuild() 594 r.intents.ResetSync() 595 r.intents.ResetDeploy() 596 597 build, sync, deploy := r.intents.GetIntentsAttrs() 598 t.CheckDeepEqual(test.expectedBuildIntent, build) 599 t.CheckDeepEqual(test.expectedSyncIntent, sync) 600 t.CheckDeepEqual(test.expectedDeployIntent, deploy) 601 }) 602 } 603 }