github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/build/test_utils.go (about) 1 package build 2 3 import ( 4 "archive/tar" 5 "context" 6 "fmt" 7 "log" 8 "os/exec" 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/docker/docker/api/types/container" 17 "github.com/stretchr/testify/assert" 18 19 "github.com/tilt-dev/clusterid" 20 wmcontainer "github.com/tilt-dev/tilt/internal/container" 21 "github.com/tilt-dev/tilt/internal/docker" 22 "github.com/tilt-dev/tilt/internal/dockerfile" 23 "github.com/tilt-dev/tilt/internal/k8s" 24 "github.com/tilt-dev/tilt/internal/testutils" 25 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 26 "github.com/tilt-dev/tilt/pkg/model" 27 ) 28 29 type dockerBuildFixture struct { 30 *tempdir.TempDirFixture 31 t testing.TB 32 ctx context.Context 33 dCli *docker.Cli 34 fakeDocker *docker.FakeClient 35 b *DockerBuilder 36 registry *exec.Cmd 37 reaper ImageReaper 38 containerIDs []wmcontainer.ID 39 ps *PipelineState 40 } 41 42 type fakeClock struct { 43 now time.Time 44 } 45 46 func (c fakeClock) Now() time.Time { return c.now } 47 48 func newDockerBuildFixture(t testing.TB) *dockerBuildFixture { 49 ctx, _, _ := testutils.CtxAndAnalyticsForTest() 50 env := clusterid.ProductGKE 51 52 kCli := k8s.NewFakeK8sClient(t) 53 kCli.Runtime = wmcontainer.RuntimeDocker 54 dEnv := docker.ProvideClusterEnv(ctx, docker.RealClientCreator{}, "gke", env, kCli, k8s.FakeMinikube{}) 55 dCli := docker.NewDockerClient(ctx, docker.Env(dEnv)) 56 _, ok := dCli.(*docker.Cli) 57 // If it wasn't an actual Docker client, it's an exploding client 58 if !ok { 59 // Call the simplest interface function that returns the error which originally occurred in NewDockerClient() 60 t.Fatal(dCli.CheckConnected()) 61 } 62 ps := NewPipelineState(ctx, 3, fakeClock{}) 63 64 labels := dockerfile.Labels(map[dockerfile.Label]dockerfile.LabelValue{ 65 TestImage: "1", 66 }) 67 ret := &dockerBuildFixture{ 68 TempDirFixture: tempdir.NewTempDirFixture(t), 69 t: t, 70 ctx: ctx, 71 dCli: dCli.(*docker.Cli), 72 b: NewDockerBuilder(dCli, labels), 73 reaper: NewImageReaper(dCli), 74 ps: ps, 75 } 76 77 t.Cleanup(ret.teardown) 78 return ret 79 } 80 81 func newFakeDockerBuildFixture(t testing.TB) *dockerBuildFixture { 82 ctx, _, _ := testutils.CtxAndAnalyticsForTest() 83 dCli := docker.NewFakeClient() 84 labels := dockerfile.Labels(map[dockerfile.Label]dockerfile.LabelValue{ 85 TestImage: "1", 86 }) 87 88 ps := NewPipelineState(ctx, 3, realClock{}) 89 90 ret := &dockerBuildFixture{ 91 TempDirFixture: tempdir.NewTempDirFixture(t), 92 t: t, 93 ctx: ctx, 94 fakeDocker: dCli, 95 b: NewDockerBuilder(dCli, labels), 96 reaper: NewImageReaper(dCli), 97 ps: ps, 98 } 99 100 t.Cleanup(ret.teardown) 101 return ret 102 } 103 104 func (f *dockerBuildFixture) teardown() { 105 for _, cID := range f.containerIDs { 106 // ignore failures 107 _ = f.dCli.ContainerRemove(f.ctx, string(cID), types.ContainerRemoveOptions{ 108 Force: true, 109 }) 110 } 111 112 // ignore failures 113 _ = f.reaper.RemoveTiltImages(f.ctx, time.Now(), true /*force*/, FilterByLabel(TestImage)) 114 115 if f.registry != nil && f.registry.Process != nil { 116 go func() { 117 err := f.registry.Process.Kill() 118 if err != nil { 119 log.Printf("killing the registry failed: %v\n", err) 120 } 121 }() 122 123 // ignore the error. we expect it to be killed 124 _ = f.registry.Wait() 125 126 _ = exec.Command("docker", "kill", "tilt-registry").Run() 127 _ = exec.Command("docker", "rm", "tilt-registry").Run() 128 } 129 } 130 131 func (f *dockerBuildFixture) getNameFromTest() wmcontainer.RefSet { 132 x := fmt.Sprintf("windmill.build/%s", strings.ToLower(f.t.Name())) 133 sel := wmcontainer.MustParseSelector(x) 134 return wmcontainer.MustSimpleRefSet(sel) 135 } 136 137 type expectedFile = testutils.ExpectedFile 138 139 func (f *dockerBuildFixture) assertImageHasLabels(ref reference.Named, expected map[string]string) { 140 inspect, _, err := f.dCli.ImageInspectWithRaw(f.ctx, ref.String()) 141 if err != nil { 142 f.t.Fatalf("error inspecting image %s: %v", ref.String(), err) 143 } 144 145 if inspect.Config == nil { 146 f.t.Fatalf("'inspect' result for image %s has nil config", ref.String()) 147 } 148 149 actual := inspect.Config.Labels 150 for k, expectV := range expected { 151 actualV, ok := actual[k] 152 if assert.True(f.t, ok, "key %q not found in actual labels: %v", k, actual) { 153 assert.Equal(f.t, expectV, actualV, "actual label (%s = %s) did not match expected (%s = %s)", 154 k, actualV, k, expectV) 155 } 156 } 157 158 } 159 160 func (f *dockerBuildFixture) assertFilesInImage(ref reference.NamedTagged, expectedFiles []expectedFile) { 161 cID := f.startContainer(f.ctx, containerConfigRunCmd(ref, model.Cmd{})) 162 f.assertFilesInContainer(f.ctx, cID, expectedFiles) 163 } 164 165 func (f *dockerBuildFixture) assertFilesInContainer( 166 ctx context.Context, cID wmcontainer.ID, expectedFiles []expectedFile) { 167 for _, expectedFile := range expectedFiles { 168 reader, _, err := f.dCli.CopyFromContainer(ctx, cID.String(), expectedFile.Path) 169 if expectedFile.Missing { 170 if err == nil { 171 f.t.Errorf("Expected path %q to not exist", expectedFile.Path) 172 } else if !strings.Contains(err.Error(), "No such container:path") && !strings.Contains(err.Error(), "Could not find the file") { 173 f.t.Errorf("Expected path %q to not exist, but got a different error: %v", expectedFile.Path, err) 174 } 175 176 continue 177 } 178 179 if err != nil { 180 f.t.Fatal(err) 181 } 182 183 // When you copy a single file out of a container, you get 184 // back a tarball with 1 entry, the file basename. 185 adjustedFile := expectedFile 186 adjustedFile.Path = filepath.Base(adjustedFile.Path) 187 testutils.AssertFileInTar(f.t, tar.NewReader(reader), adjustedFile) 188 } 189 } 190 191 // startContainer starts a container from the given config 192 func (f *dockerBuildFixture) startContainer(ctx context.Context, config *container.Config) wmcontainer.ID { 193 resp, err := f.dCli.ContainerCreate(ctx, config, nil, nil, nil, "") 194 if err != nil { 195 f.t.Fatalf("startContainer: %v", err) 196 } 197 cID := resp.ID 198 199 err = f.dCli.ContainerStart(ctx, cID, types.ContainerStartOptions{}) 200 if err != nil { 201 f.t.Fatalf("startContainer: %v", err) 202 } 203 204 result := wmcontainer.ID(cID) 205 f.containerIDs = append(f.containerIDs, result) 206 return result 207 } 208 209 // Get a container config to run a container with a given command instead of 210 // the existing entrypoint. If cmd is nil, we run nothing. 211 func containerConfigRunCmd(imgRef reference.NamedTagged, cmd model.Cmd) *container.Config { 212 config := containerConfig(imgRef) 213 214 // In Docker, both the Entrypoint and the Cmd are used to determine what 215 // process the container runtime uses, where Entrypoint takes precedence over 216 // command. We set both here to ensure that we don't get weird results due 217 // to inheritance. 218 // 219 // If cmd is nil, we use a fake cmd that does nothing. 220 // 221 // https://github.com/opencontainers/image-spec/blob/master/config.md#properties 222 if cmd.Empty() { 223 config.Cmd = model.ToUnixCmd("# NOTE(nick): a fake cmd").Argv 224 } else { 225 config.Cmd = cmd.Argv 226 } 227 config.Entrypoint = []string{} 228 return config 229 } 230 231 // Get a container config to run a container as-is. 232 func containerConfig(imgRef reference.NamedTagged) *container.Config { 233 return &container.Config{Image: imgRef.String()} 234 }