github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/build_and_deployer_test.go (about) 1 package engine 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "context" 7 "fmt" 8 "io" 9 "path/filepath" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/distribution/reference" 15 "github.com/docker/docker/api/types" 16 "github.com/opencontainers/go-digest" 17 "github.com/pkg/errors" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 22 23 "github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate" 24 "github.com/tilt-dev/tilt/internal/controllers/fake" 25 "github.com/tilt-dev/tilt/internal/engine/buildcontrol" 26 "github.com/tilt-dev/tilt/internal/localexec" 27 "github.com/tilt-dev/tilt/internal/store/liveupdates" 28 29 "github.com/tilt-dev/wmclient/pkg/dirs" 30 31 "github.com/tilt-dev/clusterid" 32 "github.com/tilt-dev/tilt/internal/container" 33 "github.com/tilt-dev/tilt/internal/dockercompose" 34 "github.com/tilt-dev/tilt/internal/k8s/testyaml" 35 "github.com/tilt-dev/tilt/internal/store" 36 37 "github.com/tilt-dev/tilt/internal/docker" 38 "github.com/tilt-dev/tilt/internal/k8s" 39 "github.com/tilt-dev/tilt/internal/testutils" 40 "github.com/tilt-dev/tilt/internal/testutils/manifestbuilder" 41 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 42 "github.com/tilt-dev/tilt/pkg/apis" 43 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 44 "github.com/tilt-dev/tilt/pkg/model" 45 ) 46 47 var testImageRef = container.MustParseNamedTagged("gcr.io/some-project-162817/sancho:deadbeef") 48 var imageTargetID = model.TargetID{ 49 Type: model.TargetTypeImage, 50 Name: model.TargetName(apis.SanitizeName("gcr.io/some-project-162817/sancho")), 51 } 52 53 var alreadyBuilt = store.NewImageBuildResultSingleRef(imageTargetID, testImageRef) 54 var alreadyBuiltSet = store.BuildResultSet{imageTargetID: alreadyBuilt} 55 56 type expectedFile = testutils.ExpectedFile 57 58 func TestGKEDeploy(t *testing.T) { 59 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 60 61 manifest := NewSanchoLiveUpdateManifest(f) 62 targets := buildcontrol.BuildTargets(manifest) 63 _, err := f.BuildAndDeploy(targets, store.BuildStateSet{}) 64 if err != nil { 65 t.Fatal(err) 66 } 67 68 if f.docker.BuildCount != 1 { 69 t.Errorf("Expected 1 docker build, actual: %d", f.docker.BuildCount) 70 } 71 72 if f.docker.PushCount != 1 { 73 t.Errorf("Expected 1 push to docker, actual: %d", f.docker.PushCount) 74 } 75 76 expectedYaml := "image: gcr.io/some-project-162817/sancho:tilt-11cd0b38bc3ceb95" 77 if !strings.Contains(f.k8s.Yaml, expectedYaml) { 78 t.Errorf("Expected yaml to contain %q. Actual:\n%s", expectedYaml, f.k8s.Yaml) 79 } 80 } 81 82 func TestYamlManifestDeploy(t *testing.T) { 83 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 84 85 manifest := manifestbuilder.New(f, "some_yaml"). 86 WithK8sYAML(testyaml.TracerYAML).Build() 87 targets := buildcontrol.BuildTargets(manifest) 88 _, err := f.BuildAndDeploy(targets, store.BuildStateSet{}) 89 if err != nil { 90 t.Fatal(err) 91 } 92 93 assert.Equal(t, 0, f.docker.BuildCount) 94 assert.Equal(t, 0, f.docker.PushCount) 95 f.assertK8sUpsertCalled(true) 96 } 97 98 func TestFallBackToImageDeploy(t *testing.T) { 99 f := newBDFixture(t, clusterid.ProductDockerDesktop, container.RuntimeDocker) 100 101 f.docker.SetExecError(errors.New("some random error")) 102 103 manifest := NewSanchoLiveUpdateManifest(f) 104 changed := f.WriteFile("a.txt", "a") 105 bs := resultToStateSet(manifest, alreadyBuiltSet, []string{changed}) 106 107 targets := buildcontrol.BuildTargets(manifest) 108 _, err := f.BuildAndDeploy(targets, bs) 109 if err != nil { 110 t.Fatal(err) 111 } 112 113 f.assertContainerRestarts(0) 114 if f.docker.BuildCount != 1 { 115 t.Errorf("Expected 1 docker build, actual: %d", f.docker.BuildCount) 116 } 117 } 118 119 func TestIgnoredFiles(t *testing.T) { 120 f := newBDFixture(t, clusterid.ProductDockerDesktop, container.RuntimeDocker) 121 122 manifest := NewSanchoDockerBuildManifest(f) 123 124 tiltfile := filepath.Join(f.Path(), "Tiltfile") 125 manifest = manifest.WithImageTarget(manifest.ImageTargetAt(0).WithIgnores([]v1alpha1.IgnoreDef{ 126 {BasePath: filepath.Join(f.Path(), ".git")}, 127 {BasePath: tiltfile}, 128 })) 129 130 f.WriteFile("Tiltfile", "# hello world") 131 f.WriteFile("a.txt", "a") 132 f.WriteFile(".git/index", "garbage") 133 134 targets := buildcontrol.BuildTargets(manifest) 135 _, err := f.BuildAndDeploy(targets, store.BuildStateSet{}) 136 if err != nil { 137 t.Fatal(err) 138 } 139 140 tr := tar.NewReader(f.docker.BuildContext) 141 testutils.AssertFilesInTar(t, tr, []expectedFile{ 142 expectedFile{ 143 Path: "a.txt", 144 Contents: "a", 145 }, 146 expectedFile{ 147 Path: ".git/index", 148 Missing: true, 149 }, 150 expectedFile{ 151 Path: "Tiltfile", 152 Missing: true, 153 }, 154 }) 155 } 156 157 func TestCustomBuild(t *testing.T) { 158 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 159 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 160 f.docker.Images["gcr.io/some-project-162817/sancho:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)} 161 162 manifest := NewSanchoCustomBuildManifest(f) 163 targets := buildcontrol.BuildTargets(manifest) 164 165 _, err := f.BuildAndDeploy(targets, store.BuildStateSet{}) 166 if err != nil { 167 t.Fatal(err) 168 } 169 170 if f.docker.BuildCount != 0 { 171 t.Errorf("Expected 0 docker build, actual: %d", f.docker.BuildCount) 172 } 173 174 if f.docker.PushCount != 1 { 175 t.Errorf("Expected 1 push to docker, actual: %d", f.docker.PushCount) 176 } 177 } 178 179 func TestCustomBuildDeterministicTag(t *testing.T) { 180 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 181 refStr := "gcr.io/some-project-162817/sancho:deterministic-tag" 182 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 183 f.docker.Images[refStr] = types.ImageInspect{ID: string(sha)} 184 185 manifest := NewSanchoCustomBuildManifestWithTag(f, "deterministic-tag") 186 targets := buildcontrol.BuildTargets(manifest) 187 188 _, err := f.BuildAndDeploy(targets, store.BuildStateSet{}) 189 if err != nil { 190 t.Fatal(err) 191 } 192 193 if f.docker.BuildCount != 0 { 194 t.Errorf("Expected 0 docker build, actual: %d", f.docker.BuildCount) 195 } 196 197 if f.docker.PushCount != 1 { 198 t.Errorf("Expected 1 push to docker, actual: %d", f.docker.PushCount) 199 } 200 } 201 202 func TestDockerComposeImageBuild(t *testing.T) { 203 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 204 205 manifest := NewSanchoLiveUpdateDCManifest(f) 206 targets := buildcontrol.BuildTargets(manifest) 207 208 _, err := f.BuildAndDeploy(targets, store.BuildStateSet{}) 209 if err != nil { 210 t.Fatal(err) 211 } 212 213 assert.Equal(t, 1, f.docker.BuildCount) 214 assert.Equal(t, 0, f.docker.PushCount) 215 assert.Empty(t, f.k8s.Yaml, "expect no k8s YAML for DockerCompose resource") 216 assert.Len(t, f.dcCli.UpCalls(), 1) 217 } 218 219 func TestReturnLastUnexpectedError(t *testing.T) { 220 f := newBDFixture(t, clusterid.ProductDockerDesktop, container.RuntimeDocker) 221 222 // next Docker build will throw an unexpected error -- this is one we want to return, 223 // even if subsequent builders throw expected errors. 224 f.docker.BuildErrorToThrow = fmt.Errorf("no one expects the unexpected error") 225 226 manifest := NewSanchoLiveUpdateManifest(f) 227 _, err := f.BuildAndDeploy(buildcontrol.BuildTargets(manifest), store.BuildStateSet{}) 228 if assert.Error(t, err) { 229 assert.Contains(t, err.Error(), "no one expects the unexpected error") 230 } 231 } 232 233 // errors get logged by the upper, so make sure our builder isn't logging the error redundantly 234 func TestDockerBuildErrorNotLogged(t *testing.T) { 235 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 236 237 // next Docker build will throw an unexpected error -- this is one we want to return, 238 // even if subsequent builders throw expected errors. 239 f.docker.BuildErrorToThrow = fmt.Errorf("no one expects the unexpected error") 240 241 manifest := NewSanchoDockerBuildManifest(f) 242 _, err := f.BuildAndDeploy(buildcontrol.BuildTargets(manifest), store.BuildStateSet{}) 243 if assert.Error(t, err) { 244 assert.Contains(t, err.Error(), "no one expects the unexpected error") 245 } 246 247 logs := f.logs.String() 248 require.Equal(t, 0, strings.Count(logs, "no one expects the unexpected error")) 249 } 250 251 func TestLocalTargetDeploy(t *testing.T) { 252 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 253 254 lt := model.NewLocalTarget("hello-world", model.ToHostCmd("echo hello world"), model.Cmd{}, nil) 255 res, err := f.BuildAndDeploy([]model.TargetSpec{lt}, store.BuildStateSet{}) 256 require.Nil(t, err) 257 258 assert.Equal(t, 0, f.docker.BuildCount, "should have 0 docker builds") 259 assert.Equal(t, 0, f.docker.PushCount, "should have 0 docker pushes") 260 assert.Empty(t, f.k8s.Yaml, "should not apply any k8s yaml") 261 assert.Len(t, res, 1, "expect exactly one result in result set") 262 assert.Contains(t, f.logs.String(), "hello world", "logs should contain cmd output") 263 } 264 265 func TestLocalTargetFailure(t *testing.T) { 266 f := newBDFixture(t, clusterid.ProductGKE, container.RuntimeDocker) 267 268 lt := model.NewLocalTarget("hello-world", model.ToHostCmd("echo 'oh no' && exit 1"), model.Cmd{}, nil) 269 res, err := f.BuildAndDeploy([]model.TargetSpec{lt}, store.BuildStateSet{}) 270 assert.Empty(t, res, "expect empty result for failed command") 271 272 require.NotNil(t, err) 273 assert.Contains(t, err.Error(), "exit status 1", "error msg should indicate command failure") 274 assert.Contains(t, f.logs.String(), "oh no", "logs should contain cmd output") 275 276 assert.Equal(t, 0, f.docker.BuildCount, "should have 0 docker builds") 277 assert.Equal(t, 0, f.docker.PushCount, "should have 0 docker pushes") 278 assert.Empty(t, f.k8s.Yaml, "should not apply any k8s yaml") 279 } 280 281 type testStore struct { 282 *store.TestingStore 283 out io.Writer 284 } 285 286 func NewTestingStore(out io.Writer) *testStore { 287 return &testStore{ 288 TestingStore: store.NewTestingStore(), 289 out: out, 290 } 291 } 292 293 func (s *testStore) Dispatch(action store.Action) { 294 s.TestingStore.Dispatch(action) 295 296 if action, ok := action.(store.LogAction); ok { 297 _, _ = s.out.Write(action.Message()) 298 } 299 } 300 301 // The API boundaries between BuildAndDeployer and the ImageBuilder aren't obvious and 302 // are likely to change in the future. So we test them together, using 303 // a fake Client and K8sClient 304 type bdFixture struct { 305 *tempdir.TempDirFixture 306 ctx context.Context 307 cancel func() 308 docker *docker.FakeClient 309 k8s *k8s.FakeK8sClient 310 bd buildcontrol.BuildAndDeployer 311 st *testStore 312 dcCli *dockercompose.FakeDCClient 313 logs *bytes.Buffer 314 ctrlClient ctrlclient.Client 315 } 316 317 func newBDFixture(t *testing.T, env clusterid.Product, runtime container.Runtime) *bdFixture { 318 return newBDFixtureWithUpdateMode(t, env, runtime, liveupdates.UpdateModeAuto) 319 } 320 321 func newBDFixtureWithUpdateMode(t *testing.T, env clusterid.Product, runtime container.Runtime, um liveupdates.UpdateMode) *bdFixture { 322 logs := new(bytes.Buffer) 323 ctx, _, ta := testutils.ForkedCtxAndAnalyticsForTest(logs) 324 ctx, cancel := context.WithCancel(ctx) 325 f := tempdir.NewTempDirFixture(t) 326 dir := dirs.NewTiltDevDirAt(f.Path()) 327 dockerClient := docker.NewFakeClient() 328 dockerClient.ContainerListOutput = map[string][]types.Container{ 329 "pod": []types.Container{ 330 types.Container{ 331 ID: k8s.MagicTestContainerID, 332 }, 333 }, 334 } 335 k8s := k8s.NewFakeK8sClient(t) 336 k8s.Runtime = runtime 337 mode := liveupdates.UpdateModeFlag(um) 338 dcc := dockercompose.NewFakeDockerComposeClient(t, ctx) 339 kl := &fakeKINDLoader{} 340 ctrlClient := fake.NewFakeTiltClient() 341 st := NewTestingStore(logs) 342 execer := localexec.NewFakeExecer(t) 343 bd, err := provideFakeBuildAndDeployer(ctx, dockerClient, k8s, dir, env, mode, dcc, 344 fakeClock{now: time.Unix(1551202573, 0)}, kl, ta, ctrlClient, st, execer) 345 require.NoError(t, err) 346 347 ret := &bdFixture{ 348 TempDirFixture: f, 349 ctx: ctx, 350 cancel: cancel, 351 docker: dockerClient, 352 k8s: k8s, 353 bd: bd, 354 st: st, 355 dcCli: dcc, 356 logs: logs, 357 ctrlClient: ctrlClient, 358 } 359 360 t.Cleanup(ret.TearDown) 361 return ret 362 } 363 364 func (f *bdFixture) TearDown() { 365 f.cancel() 366 } 367 368 func (f *bdFixture) NewPathSet(paths ...string) model.PathSet { 369 return model.NewPathSet(paths, f.Path()) 370 } 371 372 func (f *bdFixture) assertContainerRestarts(count int) { 373 // Ensure that MagicTestContainerID was the only container id that saw 374 // restarts, and that it saw the right number of restarts. 375 expected := map[string]int{} 376 if count != 0 { 377 expected[string(k8s.MagicTestContainerID)] = count 378 } 379 assert.Equal(f.T(), expected, f.docker.RestartsByContainer, 380 "checking for expected # of container restarts") 381 } 382 383 func (f *bdFixture) assertK8sUpsertCalled(called bool) { 384 assert.Equal(f.T(), called, f.k8s.Yaml != "", 385 "checking that k8s.Upsert was called") 386 } 387 388 func (f *bdFixture) upsertSpec(obj ctrlclient.Object) { 389 fake.UpsertSpec(f.ctx, f.T(), f.ctrlClient, obj) 390 } 391 392 func (f *bdFixture) updateStatus(obj ctrlclient.Object) { 393 fake.UpdateStatus(f.ctx, f.T(), f.ctrlClient, obj) 394 } 395 396 func (f *bdFixture) BuildAndDeploy(specs []model.TargetSpec, stateSet store.BuildStateSet) (store.BuildResultSet, error) { 397 cluster := &v1alpha1.Cluster{} 398 for _, spec := range specs { 399 switch spec.(type) { 400 case model.DockerComposeTarget: 401 cluster.Spec.Connection = &v1alpha1.ClusterConnection{ 402 Docker: &v1alpha1.DockerClusterConnection{}, 403 } 404 case model.K8sTarget: 405 cluster.Spec.Connection = &v1alpha1.ClusterConnection{ 406 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 407 } 408 } 409 } 410 411 for _, spec := range specs { 412 localTarget, ok := spec.(model.LocalTarget) 413 if ok && localTarget.UpdateCmdSpec != nil { 414 cmd := v1alpha1.Cmd{ 415 ObjectMeta: metav1.ObjectMeta{Name: localTarget.UpdateCmdName()}, 416 Spec: *(localTarget.UpdateCmdSpec), 417 } 418 f.upsertSpec(&cmd) 419 } 420 421 iTarget, ok := spec.(model.ImageTarget) 422 if ok { 423 im := v1alpha1.ImageMap{ 424 ObjectMeta: metav1.ObjectMeta{Name: iTarget.ID().Name.String()}, 425 Spec: iTarget.ImageMapSpec, 426 } 427 f.upsertSpec(&im) 428 state := stateSet[iTarget.ID()] 429 state.Cluster = cluster 430 stateSet[iTarget.ID()] = state 431 432 imageBuildResult, ok := state.LastResult.(store.ImageBuildResult) 433 if ok { 434 im.Status = imageBuildResult.ImageMapStatus 435 } 436 f.updateStatus(&im) 437 438 if !liveupdate.IsEmptySpec(iTarget.LiveUpdateSpec) { 439 lu := v1alpha1.LiveUpdate{ 440 ObjectMeta: metav1.ObjectMeta{Name: iTarget.LiveUpdateName}, 441 Spec: iTarget.LiveUpdateSpec, 442 } 443 f.upsertSpec(&lu) 444 } 445 446 if iTarget.IsDockerBuild() { 447 di := v1alpha1.DockerImage{ 448 ObjectMeta: metav1.ObjectMeta{Name: iTarget.DockerImageName}, 449 Spec: iTarget.DockerBuildInfo().DockerImageSpec, 450 } 451 f.upsertSpec(&di) 452 } 453 if iTarget.IsCustomBuild() { 454 cmdImageSpec := iTarget.CustomBuildInfo().CmdImageSpec 455 ci := v1alpha1.CmdImage{ 456 ObjectMeta: metav1.ObjectMeta{Name: iTarget.CmdImageName}, 457 Spec: cmdImageSpec, 458 } 459 f.upsertSpec(&ci) 460 461 c := v1alpha1.Cmd{ 462 ObjectMeta: metav1.ObjectMeta{Name: iTarget.CmdImageName}, 463 Spec: v1alpha1.CmdSpec{ 464 Args: cmdImageSpec.Args, 465 Dir: cmdImageSpec.Dir, 466 }, 467 } 468 f.upsertSpec(&c) 469 } 470 } 471 472 kTarget, ok := spec.(model.K8sTarget) 473 if ok { 474 ka := v1alpha1.KubernetesApply{ 475 ObjectMeta: metav1.ObjectMeta{Name: kTarget.ID().Name.String()}, 476 Spec: kTarget.KubernetesApplySpec, 477 } 478 f.upsertSpec(&ka) 479 } 480 481 dcTarget, ok := spec.(model.DockerComposeTarget) 482 if ok { 483 dcs := v1alpha1.DockerComposeService{ 484 ObjectMeta: metav1.ObjectMeta{Name: dcTarget.ID().Name.String()}, 485 Spec: dcTarget.Spec, 486 } 487 f.upsertSpec(&dcs) 488 } 489 } 490 return f.bd.BuildAndDeploy(f.ctx, f.st, specs, stateSet) 491 } 492 493 func resultToStateSet(m model.Manifest, resultSet store.BuildResultSet, files []string) store.BuildStateSet { 494 stateSet := store.BuildStateSet{} 495 for id, result := range resultSet { 496 stateSet[id] = store.NewBuildState(result, files, nil) 497 } 498 return stateSet 499 } 500 501 type fakeClock struct { 502 now time.Time 503 } 504 505 func (c fakeClock) Now() time.Time { return c.now } 506 507 type fakeKINDLoader struct { 508 loadCount int 509 } 510 511 func (kl *fakeKINDLoader) LoadToKIND(ctx context.Context, cluster *v1alpha1.Cluster, ref reference.NamedTagged) error { 512 kl.loadCount++ 513 return nil 514 }