github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/docker_compose_build_and_deployer_test.go (about)

     1  package buildcontrol
     2  
     3  import (
     4  	"archive/tar"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"testing"
     9  
    10  	"github.com/jonboulle/clockwork"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    15  
    16  	"github.com/tilt-dev/wmclient/pkg/dirs"
    17  
    18  	"github.com/tilt-dev/tilt/internal/container"
    19  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    20  	"github.com/tilt-dev/tilt/internal/docker"
    21  	"github.com/tilt-dev/tilt/internal/dockercompose"
    22  	"github.com/tilt-dev/tilt/internal/store"
    23  	"github.com/tilt-dev/tilt/internal/testutils"
    24  	"github.com/tilt-dev/tilt/internal/testutils/manifestbuilder"
    25  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    26  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    27  	"github.com/tilt-dev/tilt/pkg/model"
    28  )
    29  
    30  func TestDockerComposeTargetBuilt(t *testing.T) {
    31  	f := newDCBDFixture(t)
    32  
    33  	expectedContainerID := "fake-container-id"
    34  	f.dcCli.ContainerIDDefault = container.ID(expectedContainerID)
    35  
    36  	manifest := manifestbuilder.New(f, "fe").WithDockerCompose().Build()
    37  	dcTarg := manifest.DockerComposeTarget()
    38  
    39  	res, err := f.BuildAndDeploy(BuildTargets(manifest), store.BuildStateSet{})
    40  	if err != nil {
    41  		t.Fatal(err)
    42  	}
    43  	upCalls := f.dcCli.UpCalls()
    44  	if assert.Len(t, upCalls, 1, "expect one call to `docker-compose up`") {
    45  		call := upCalls[0]
    46  		assert.Equal(t, dcTarg.Spec, call.Spec)
    47  		assert.Equal(t, "fe", call.Spec.Service)
    48  		assert.True(t, call.ShouldBuild)
    49  	}
    50  
    51  	dRes := res[dcTarg.ID()].(store.DockerComposeBuildResult)
    52  	assert.Equal(t, expectedContainerID, dRes.Status.ContainerID)
    53  }
    54  
    55  func TestTiltBuildsImage(t *testing.T) {
    56  	f := newDCBDFixture(t)
    57  
    58  	iTarget := NewSanchoDockerBuildImageTarget(f)
    59  	manifest := manifestbuilder.New(f, "fe").
    60  		WithDockerCompose().
    61  		WithImageTarget(iTarget).
    62  		Build()
    63  	dcTarg := manifest.DockerComposeTarget()
    64  
    65  	res, err := f.BuildAndDeploy(BuildTargets(manifest), store.BuildStateSet{})
    66  	if err != nil {
    67  		t.Fatal(err)
    68  	}
    69  
    70  	assert.Equal(t, 1, f.dCli.BuildCount, "expect one docker build")
    71  
    72  	expectedTag := fmt.Sprintf("%s:%s", iTarget.ImageMapSpec.Selector, docker.TagLatest)
    73  	assert.Equal(t, expectedTag, f.dCli.TagTarget)
    74  
    75  	upCalls := f.dcCli.UpCalls()
    76  	if assert.Len(t, upCalls, 1, "expect one call to `docker-compose up`") {
    77  		call := upCalls[0]
    78  		assert.Equal(t, dcTarg.Spec, call.Spec)
    79  		assert.Equal(t, "fe", call.Spec.Service)
    80  		assert.False(t, call.ShouldBuild, "should call `up` without `--build` b/c Tilt is doing the building")
    81  	}
    82  
    83  	assert.Len(t, res, 2, "expect two results (one for each spec)")
    84  }
    85  
    86  func TestTiltBuildsImageWithTag(t *testing.T) {
    87  	f := newDCBDFixture(t)
    88  
    89  	refWithTag := "gcr.io/foo:bar"
    90  	iTarget := model.MustNewImageTarget(container.MustParseSelector(refWithTag)).
    91  		WithBuildDetails(model.DockerBuild{DockerImageSpec: v1alpha1.DockerImageSpec{Context: "-"}})
    92  	manifest := manifestbuilder.New(f, "fe").
    93  		WithDockerCompose().
    94  		WithImageTarget(iTarget).
    95  		Build()
    96  
    97  	_, err := f.BuildAndDeploy(BuildTargets(manifest), store.BuildStateSet{})
    98  	if err != nil {
    99  		t.Fatal(err)
   100  	}
   101  
   102  	assert.Equal(t, refWithTag, f.dCli.TagTarget)
   103  }
   104  
   105  func TestDCBADRejectsAllSpecsIfOneUnsupported(t *testing.T) {
   106  	f := newDCBDFixture(t)
   107  
   108  	specs := []model.TargetSpec{model.DockerComposeTarget{}, model.ImageTarget{}, model.K8sTarget{}}
   109  
   110  	plan, err := f.dcbad.extract(specs)
   111  	assert.Empty(t, plan)
   112  	assert.EqualError(t, err, "DockerComposeBuildAndDeployer does not support target type model.K8sTarget")
   113  }
   114  
   115  func TestMultiStageDockerCompose(t *testing.T) {
   116  	f := newDCBDFixture(t)
   117  
   118  	manifest := NewSanchoDockerBuildMultiStageManifest(f).
   119  		WithDeployTarget(defaultDockerComposeTarget(f, "sancho"))
   120  
   121  	stateSet := store.BuildStateSet{}
   122  	_, err := f.BuildAndDeploy(BuildTargets(manifest), stateSet)
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  
   127  	assert.Equal(t, 2, f.dCli.BuildCount)
   128  	assert.Equal(t, 0, f.dCli.PushCount)
   129  
   130  	expected := testutils.ExpectedFile{
   131  		Path: "Dockerfile",
   132  		Contents: `
   133  FROM sancho-base:latest
   134  ADD . .
   135  RUN go install github.com/tilt-dev/sancho
   136  ENTRYPOINT /go/bin/sancho
   137  `,
   138  	}
   139  	testutils.AssertFileInTar(t, tar.NewReader(f.dCli.BuildContext), expected)
   140  }
   141  
   142  func TestMultiStageDockerComposeWithOnlyOneDirtyImage(t *testing.T) {
   143  	f := newDCBDFixture(t)
   144  
   145  	manifest := NewSanchoDockerBuildMultiStageManifest(f).
   146  		WithDeployTarget(defaultDockerComposeTarget(f, "sancho"))
   147  
   148  	iTargetID := manifest.ImageTargets[0].ID()
   149  	result := store.NewImageBuildResultSingleRef(iTargetID, container.MustParseNamedTagged("sancho-base:tilt-prebuilt"))
   150  	state := store.NewBuildState(result, nil, nil)
   151  	stateSet := store.BuildStateSet{iTargetID: state}
   152  	_, err := f.BuildAndDeploy(BuildTargets(manifest), stateSet)
   153  	if err != nil {
   154  		t.Fatal(err)
   155  	}
   156  
   157  	assert.Equal(t, 1, f.dCli.BuildCount)
   158  	assert.Equal(t, 0, f.dCli.PushCount)
   159  
   160  	expected := testutils.ExpectedFile{
   161  		Path: "Dockerfile",
   162  		Contents: `
   163  FROM sancho-base:tilt-prebuilt
   164  ADD . .
   165  RUN go install github.com/tilt-dev/sancho
   166  ENTRYPOINT /go/bin/sancho
   167  `,
   168  	}
   169  	testutils.AssertFileInTar(t, tar.NewReader(f.dCli.BuildContext), expected)
   170  }
   171  
   172  func TestForceUpdateDC(t *testing.T) {
   173  	f := newDCBDFixture(t)
   174  
   175  	m := manifestbuilder.New(f, "fe").WithDockerCompose().Build()
   176  	iTargetID1 := m.ImageTargets[0].ID()
   177  	stateSet := store.BuildStateSet{
   178  		iTargetID1: store.BuildState{FullBuildTriggered: true},
   179  	}
   180  
   181  	_, err := f.BuildAndDeploy(BuildTargets(m), stateSet)
   182  	require.NoError(t, err)
   183  
   184  	// A force rebuild should delete the old resources.
   185  	assert.Equal(t, 1, len(f.dcCli.RmCalls()))
   186  }
   187  
   188  type dcbdFixture struct {
   189  	*tempdir.TempDirFixture
   190  	ctx        context.Context
   191  	dcCli      *dockercompose.FakeDCClient
   192  	dCli       *docker.FakeClient
   193  	dcbad      *DockerComposeBuildAndDeployer
   194  	st         *store.TestingStore
   195  	ctrlClient ctrlclient.Client
   196  }
   197  
   198  func newDCBDFixture(t *testing.T) *dcbdFixture {
   199  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   200  
   201  	f := tempdir.NewTempDirFixture(t)
   202  
   203  	// empty dirs for build contexts
   204  	_ = os.Mkdir(f.JoinPath("sancho"), 0777)
   205  	_ = os.Mkdir(f.JoinPath("sancho-base"), 0777)
   206  
   207  	dir := dirs.NewTiltDevDirAt(f.Path())
   208  	dcCli := dockercompose.NewFakeDockerComposeClient(t, ctx)
   209  	dCli := docker.NewFakeClient()
   210  	cdc := fake.NewFakeTiltClient()
   211  	st := store.NewTestingStore()
   212  
   213  	// Make the fake ImageExists always return true, which is the behavior we want
   214  	// when testing the BuildAndDeployers.
   215  	dCli.ImageAlwaysExists = true
   216  
   217  	clock := clockwork.NewFakeClock()
   218  	dcbad, err := ProvideDockerComposeBuildAndDeployer(ctx, dcCli, dCli, cdc, st, clock, dir)
   219  	if err != nil {
   220  		t.Fatal(err)
   221  	}
   222  	return &dcbdFixture{
   223  		TempDirFixture: f,
   224  		ctx:            ctx,
   225  		dcCli:          dcCli,
   226  		dCli:           dCli,
   227  		dcbad:          dcbad,
   228  		st:             st,
   229  		ctrlClient:     cdc,
   230  	}
   231  }
   232  
   233  func (f *dcbdFixture) upsertSpec(obj ctrlclient.Object) {
   234  	fake.UpsertSpec(f.ctx, f.T(), f.ctrlClient, obj)
   235  }
   236  
   237  func (f *dcbdFixture) updateStatus(obj ctrlclient.Object) {
   238  	fake.UpdateStatus(f.ctx, f.T(), f.ctrlClient, obj)
   239  }
   240  
   241  func (f *dcbdFixture) BuildAndDeploy(specs []model.TargetSpec, stateSet store.BuildStateSet) (store.BuildResultSet, error) {
   242  
   243  	cluster := &v1alpha1.Cluster{
   244  		Spec: v1alpha1.ClusterSpec{
   245  			Connection: &v1alpha1.ClusterConnection{
   246  				Docker: &v1alpha1.DockerClusterConnection{},
   247  			},
   248  		},
   249  	}
   250  
   251  	for _, spec := range specs {
   252  		iTarget, ok := spec.(model.ImageTarget)
   253  		if !ok || iTarget.IsLiveUpdateOnly {
   254  			continue
   255  		}
   256  
   257  		im := v1alpha1.ImageMap{
   258  			ObjectMeta: metav1.ObjectMeta{Name: iTarget.ID().Name.String()},
   259  			Spec:       iTarget.ImageMapSpec,
   260  		}
   261  		f.upsertSpec(&im)
   262  
   263  		state := stateSet[iTarget.ID()]
   264  		state.Cluster = cluster
   265  		stateSet[iTarget.ID()] = state
   266  
   267  		imageBuildResult, ok := state.LastResult.(store.ImageBuildResult)
   268  		if ok {
   269  			im.Status = imageBuildResult.ImageMapStatus
   270  		}
   271  		f.updateStatus(&im)
   272  	}
   273  	return f.dcbad.BuildAndDeploy(f.ctx, f.st, specs, stateSet)
   274  }
   275  
   276  func defaultDockerComposeTarget(f Fixture, name string) model.DockerComposeTarget {
   277  	return model.DockerComposeTarget{
   278  		Name: model.TargetName(name),
   279  		Spec: v1alpha1.DockerComposeServiceSpec{
   280  			Service: name,
   281  		},
   282  	}
   283  }