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  }