github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/build/custom_builder_test.go (about) 1 package build 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "runtime" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/docker/docker/api/types" 13 "github.com/jonboulle/clockwork" 14 "github.com/opencontainers/go-digest" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 ktypes "k8s.io/apimachinery/pkg/types" 19 20 "github.com/tilt-dev/tilt/internal/container" 21 "github.com/tilt-dev/tilt/internal/controllers/core/cmd" 22 "github.com/tilt-dev/tilt/internal/controllers/fake" 23 "github.com/tilt-dev/tilt/internal/docker" 24 "github.com/tilt-dev/tilt/internal/localexec" 25 "github.com/tilt-dev/tilt/internal/store" 26 "github.com/tilt-dev/tilt/internal/testutils" 27 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 28 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 29 "github.com/tilt-dev/tilt/pkg/model" 30 ) 31 32 var defaultCluster = &v1alpha1.Cluster{ 33 ObjectMeta: metav1.ObjectMeta{Name: "default"}, 34 } 35 var TwoURLRegistry = &v1alpha1.RegistryHosting{ 36 Host: "localhost:1234", 37 HostFromContainerRuntime: "registry:1234", 38 } 39 40 func TestCustomBuildSuccess(t *testing.T) { 41 f := newFakeCustomBuildFixture(t) 42 43 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 44 f.dCli.Images["gcr.io/foo/bar:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)} 45 cb := f.customBuild("exit 0") 46 refs, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 47 require.NoError(t, err) 48 49 assert.Equal(f.t, container.MustParseNamed("gcr.io/foo/bar:tilt-11cd0eb38bc3ceb9"), refs.LocalRef) 50 assert.Equal(f.t, container.MustParseNamed("gcr.io/foo/bar:tilt-11cd0eb38bc3ceb9"), refs.ClusterRef) 51 } 52 53 func TestCustomBuildSuccessClusterRefTaggedWithDigest(t *testing.T) { 54 f := newFakeCustomBuildFixture(t) 55 56 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 57 f.dCli.Images["localhost:1234/foo_bar:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)} 58 cb := f.customBuild("exit 0") 59 refs, err := f.Build(refSetWithRegistryFromString("foo/bar", TwoURLRegistry), cb, nil) 60 require.NoError(t, err) 61 62 assert.Equal(f.t, container.MustParseNamed("localhost:1234/foo_bar:tilt-11cd0eb38bc3ceb9"), refs.LocalRef) 63 assert.Equal(f.t, container.MustParseNamed("registry:1234/foo_bar:tilt-11cd0eb38bc3ceb9"), refs.ClusterRef) 64 } 65 66 func TestCustomBuildSuccessClusterRefWithCustomTag(t *testing.T) { 67 f := newFakeCustomBuildFixture(t) 68 69 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 70 f.dCli.Images["gcr.io/foo/bar:my-tag"] = types.ImageInspect{ID: string(sha)} 71 cb := f.customBuild("exit 0") 72 cb.CmdImageSpec.OutputTag = "my-tag" 73 refs, err := f.Build(refSetWithRegistryFromString("gcr.io/foo/bar", TwoURLRegistry), cb, nil) 74 require.NoError(t, err) 75 76 assert.Equal(f.t, container.MustParseNamed("localhost:1234/gcr.io_foo_bar:tilt-11cd0eb38bc3ceb9"), refs.LocalRef) 77 assert.Equal(f.t, container.MustParseNamed("registry:1234/gcr.io_foo_bar:tilt-11cd0eb38bc3ceb9"), refs.ClusterRef) 78 } 79 80 func TestCustomBuildSuccessSkipsLocalDocker(t *testing.T) { 81 f := newFakeCustomBuildFixture(t) 82 83 cb := f.customBuild("exit 0") 84 cb.CmdImageSpec.OutputMode = v1alpha1.CmdImageOutputRemote 85 refs, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 86 require.NoError(f.t, err) 87 88 assert.Equal(f.t, container.MustParseNamed("gcr.io/foo/bar:tilt-build-1551202573"), refs.LocalRef) 89 assert.Equal(f.t, container.MustParseNamed("gcr.io/foo/bar:tilt-build-1551202573"), refs.ClusterRef) 90 } 91 92 func TestCustomBuildSuccessClusterRefTaggedIfSkipsLocalDocker(t *testing.T) { 93 f := newFakeCustomBuildFixture(t) 94 95 cb := f.customBuild("exit 0") 96 cb.CmdImageSpec.OutputMode = v1alpha1.CmdImageOutputRemote 97 refs, err := f.Build(refSetWithRegistryFromString("foo/bar", TwoURLRegistry), cb, nil) 98 require.NoError(f.t, err) 99 100 assert.Equal(f.t, container.MustParseNamed("localhost:1234/foo_bar:tilt-build-1551202573"), refs.LocalRef) 101 assert.Equal(f.t, container.MustParseNamed("registry:1234/foo_bar:tilt-build-1551202573"), refs.ClusterRef) 102 } 103 104 func TestCustomBuildCmdFails(t *testing.T) { 105 f := newFakeCustomBuildFixture(t) 106 107 cb := f.customBuild("exit 1") 108 _, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 109 // TODO(dmiller) better error message 110 assert.EqualError(t, err, "Custom build \"exit 1\" failed: exit status 1") 111 } 112 113 func TestCustomBuildImgNotFound(t *testing.T) { 114 f := newFakeCustomBuildFixture(t) 115 116 cb := f.customBuild("exit 0") 117 _, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 118 assert.Contains(t, err.Error(), "fake docker client error: object not found") 119 } 120 121 func TestCustomBuildExpectedTag(t *testing.T) { 122 f := newFakeCustomBuildFixture(t) 123 124 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 125 f.dCli.Images["gcr.io/foo/bar:the-tag"] = types.ImageInspect{ID: string(sha)} 126 127 cb := f.customBuild("exit 0") 128 cb.CmdImageSpec.OutputTag = "the-tag" 129 refs, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 130 require.NoError(t, err) 131 132 assert.Equal(f.t, container.MustParseNamed("gcr.io/foo/bar:tilt-11cd0eb38bc3ceb9"), refs.LocalRef) 133 assert.Equal(f.t, container.MustParseNamed("gcr.io/foo/bar:tilt-11cd0eb38bc3ceb9"), refs.ClusterRef) 134 } 135 136 func TestCustomBuilderExecsRelativeToTiltfile(t *testing.T) { 137 if runtime.GOOS == "windows" { 138 t.Skip("no sh on windows") 139 } 140 f := newFakeCustomBuildFixture(t) 141 142 f.WriteFile("proj/build.sh", "exit 0") 143 144 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 145 f.dCli.Images["gcr.io/foo/bar:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)} 146 cb := f.customBuild("./build.sh") 147 cb.CmdImageSpec.Dir = filepath.Join(f.Path(), "proj") 148 refs, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 149 if err != nil { 150 f.t.Fatal(err) 151 } 152 153 assert.Equal(f.t, container.MustParseNamed("gcr.io/foo/bar:tilt-11cd0eb38bc3ceb9"), refs.LocalRef) 154 } 155 156 func TestCustomBuildOutputsToImageRefSuccess(t *testing.T) { 157 f := newFakeCustomBuildFixture(t) 158 159 myTag := "gcr.io/foo/bar:dev" 160 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 161 f.dCli.Images[myTag] = types.ImageInspect{ID: string(sha)} 162 cb := f.customBuild("echo gcr.io/foo/bar:dev > ref.txt") 163 cb.CmdImageSpec.OutputsImageRefTo = f.JoinPath("ref.txt") 164 refs, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 165 require.NoError(t, err) 166 167 assert.Equal(f.t, container.MustParseNamed(myTag), refs.LocalRef) 168 assert.Equal(f.t, container.MustParseNamed(myTag), refs.ClusterRef) 169 } 170 171 func TestCustomBuildOutputsToImageRefMissingImage(t *testing.T) { 172 f := newFakeCustomBuildFixture(t) 173 174 myTag := "gcr.io/foo/bar:dev" 175 cb := f.customBuild(fmt.Sprintf("echo %s > ref.txt", myTag)) 176 cb.CmdImageSpec.OutputsImageRefTo = f.JoinPath("ref.txt") 177 _, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 178 require.NotNil(t, err) 179 assert.Contains(t, err.Error(), 180 fmt.Sprintf("fake docker client error: object not found (fakeClient.Images key: %s)", myTag)) 181 } 182 183 func TestCustomBuildOutputsToImageRefMalformedImage(t *testing.T) { 184 f := newFakeCustomBuildFixture(t) 185 186 cb := f.customBuild("echo 999 > ref.txt") 187 cb.CmdImageSpec.OutputsImageRefTo = f.JoinPath("ref.txt") 188 _, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 189 require.NotNil(t, err) 190 assert.Contains(t, err.Error(), 191 fmt.Sprintf("Output image ref in file %s was invalid: Expected reference \"999\" to contain a tag", 192 f.JoinPath("ref.txt"))) 193 } 194 195 func TestCustomBuildOutputsToImageRefSkipsLocalDocker(t *testing.T) { 196 f := newFakeCustomBuildFixture(t) 197 198 myTag := "gcr.io/foo/bar:dev" 199 cb := f.customBuild(fmt.Sprintf("echo %s > ref.txt", myTag)) 200 cb.CmdImageSpec.OutputsImageRefTo = f.JoinPath("ref.txt") 201 cb.CmdImageSpec.OutputMode = v1alpha1.CmdImageOutputRemote 202 refs, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, nil) 203 require.NoError(t, err) 204 assert.Equal(f.t, container.MustParseNamed(myTag), refs.LocalRef) 205 assert.Equal(f.t, container.MustParseNamed(myTag), refs.ClusterRef) 206 } 207 208 func TestCustomBuildOutputsToImageRef_DifferentClusterHost(t *testing.T) { 209 f := newFakeCustomBuildFixture(t) 210 211 myTag := "localhost:5000/foo/bar:dev" 212 myClusterTag := "registry:5000/foo/bar:dev" 213 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 214 f.dCli.Images[myTag] = types.ImageInspect{ID: string(sha)} 215 cb := f.customBuild(fmt.Sprintf("echo %s > ref.txt", myTag)) 216 cb.CmdImageSpec.OutputsImageRefTo = f.JoinPath("ref.txt") 217 reg := &v1alpha1.RegistryHosting{Host: "localhost:5000", HostFromContainerRuntime: "registry:5000"} 218 refs, err := f.Build(refSetWithRegistryFromString("localhost:5000/foo/bar", reg), cb, nil) 219 require.NoError(t, err) 220 assert.Equal(f.t, container.MustParseNamed(myTag), refs.LocalRef) 221 assert.Equal(f.t, container.MustParseNamed(myClusterTag), refs.ClusterRef) 222 } 223 224 func TestCustomBuildImageDep(t *testing.T) { 225 if runtime.GOOS == "windows" { 226 t.Skip("no sh on windows") 227 } 228 229 f := newFakeCustomBuildFixture(t) 230 231 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 232 f.dCli.Images["gcr.io/foo/bar:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)} 233 cb := f.customBuild("echo $TILT_IMAGE_0 > image-0.txt") 234 cb.CmdImageSpec.ImageMaps = []string{"base"} 235 236 imageMaps := map[ktypes.NamespacedName]*v1alpha1.ImageMap{ 237 ktypes.NamespacedName{Name: "base"}: &v1alpha1.ImageMap{ 238 Status: v1alpha1.ImageMapStatus{ 239 ImageFromLocal: "base:tilt-12345", 240 }, 241 }, 242 } 243 244 _, err := f.Build(refSetFromString("gcr.io/foo/bar"), cb, imageMaps) 245 require.NoError(t, err) 246 247 assert.Equal(f.t, "base:tilt-12345", strings.TrimSpace(f.ReadFile("image-0.txt"))) 248 } 249 250 func TestCustomBuildEnvVars(t *testing.T) { 251 if runtime.GOOS == "windows" { 252 t.Skip("no sh on windows") 253 } 254 255 expectedVars := map[string]string{ 256 "EXPECTED_REF": "localhost:1234/foo_bar:tilt-build-1551202573", 257 "EXPECTED_REGISTRY": "localhost:1234", 258 "EXPECTED_IMAGE": "foo_bar", 259 "EXPECTED_TAG": "tilt-build-1551202573", 260 "REGISTRY_HOST": "localhost:1234", 261 "EXTRA": "value", 262 } 263 var script []string 264 for k, v := range expectedVars { 265 script = append(script, fmt.Sprintf( 266 `if [ "${%s}" != "%s" ]; then >&2 printf "%s:\n\texpected: %s\n\tactual: ${%s}\n"; exit 1; fi`, 267 k, v, k, v, k)) 268 } 269 270 f := newFakeCustomBuildFixture(t) 271 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 272 f.dCli.Images["localhost:1234/foo_bar:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)} 273 cb := f.customBuild(strings.Join(script, "\n")) 274 cb.Env = []string{"EXTRA=value"} 275 _, err := f.Build(refSetWithRegistryFromString("foo/bar", TwoURLRegistry), cb, nil) 276 require.NoError(t, err) 277 } 278 279 func TestCustomBuildEnvVars_ConfigRefWithLocalRegistry(t *testing.T) { 280 if runtime.GOOS == "windows" { 281 t.Skip("no sh on windows") 282 } 283 284 // generally, config refs (value in Tiltfile) are $prod_registry/$image:$tag 285 // and Tilt rewrites it to $local_registry/$sanitized_prod_registry_$image 286 // however, some users explicitly use the $local_registry in their Tiltfile 287 // refs, so instead of producing a redundant and confusing ref like 288 // $local_registry/$sanitized_local_registry_$image, it just gets passed 289 // through 290 expectedVars := map[string]string{ 291 "EXPECTED_REF": "localhost:1234/foo/bar:tilt-build-1551202573", 292 "EXPECTED_REGISTRY": "localhost:1234", 293 "EXPECTED_IMAGE": "foo/bar", 294 "EXPECTED_TAG": "tilt-build-1551202573", 295 "REGISTRY_HOST": "localhost:1234", 296 } 297 var script []string 298 for k, v := range expectedVars { 299 script = append(script, fmt.Sprintf( 300 `if [ "${%s}" != "%s" ]; then >&2 printf "%s:\n\texpected: %s\n\tactual: ${%s}\n"; exit 1; fi`, 301 k, v, k, v, k)) 302 } 303 304 f := newFakeCustomBuildFixture(t) 305 sha := digest.Digest("sha256:11cd0eb38bc3ceb958ffb2f9bd70be3fb317ce7d255c8a4c3f4af30e298aa1aab") 306 f.dCli.Images["localhost:1234/foo/bar:tilt-build-1551202573"] = types.ImageInspect{ID: string(sha)} 307 cb := f.customBuild(strings.Join(script, "\n")) 308 _, err := f.Build(refSetWithRegistryFromString("localhost:1234/foo/bar", TwoURLRegistry), cb, nil) 309 require.NoError(t, err) 310 } 311 312 type fakeCustomBuildFixture struct { 313 *tempdir.TempDirFixture 314 315 t *testing.T 316 ctx context.Context 317 dCli *docker.FakeClient 318 cb *CustomBuilder 319 } 320 321 func newFakeCustomBuildFixture(t *testing.T) *fakeCustomBuildFixture { 322 ctx, _, _ := testutils.CtxAndAnalyticsForTest() 323 dCli := docker.NewFakeClient() 324 clock := fakeClock{ 325 now: time.Unix(1551202573, 0), 326 } 327 328 ctrlClient := fake.NewFakeTiltClient() 329 fe := cmd.NewProcessExecer(localexec.EmptyEnv()) 330 fpm := cmd.NewFakeProberManager() 331 cclock := clockwork.NewFakeClock() 332 st := store.NewTestingStore() 333 cmds := cmd.NewController(ctx, fe, fpm, ctrlClient, st, cclock, v1alpha1.NewScheme()) 334 cb := NewCustomBuilder(dCli, clock, cmds) 335 336 return &fakeCustomBuildFixture{ 337 TempDirFixture: tempdir.NewTempDirFixture(t), 338 t: t, 339 ctx: ctx, 340 dCli: dCli, 341 cb: cb, 342 } 343 } 344 345 func (f *fakeCustomBuildFixture) customBuild(args string) model.CustomBuild { 346 return model.CustomBuild{ 347 CmdImageSpec: v1alpha1.CmdImageSpec{ 348 Args: model.ToHostCmd(args).Argv, 349 Dir: f.Path(), 350 }, 351 } 352 } 353 354 func (f *fakeCustomBuildFixture) Build(refs container.RefSet, cb model.CustomBuild, imageMaps map[ktypes.NamespacedName]*v1alpha1.ImageMap) (container.TaggedRefs, error) { 355 return f.cb.Build(f.ctx, refs, cb.CmdImageSpec, &v1alpha1.Cmd{ 356 ObjectMeta: metav1.ObjectMeta{Name: "img"}, 357 Spec: v1alpha1.CmdSpec{ 358 Args: cb.CmdImageSpec.Args, 359 Dir: cb.CmdImageSpec.Dir, 360 }, 361 }, imageMaps) 362 } 363 364 func refSetFromString(s string) container.RefSet { 365 sel := container.MustParseSelector(s) 366 return container.MustSimpleRefSet(sel) 367 } 368 369 func refSetWithRegistryFromString(ref string, reg *v1alpha1.RegistryHosting) container.RefSet { 370 r, err := container.NewRefSet(container.MustParseSelector(ref), reg) 371 if err != nil { 372 panic(err) 373 } 374 return r 375 }