github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/dockercompose/client_test.go (about)

     1  package dockercompose
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/compose-spec/compose-go/types"
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/tilt-dev/tilt/internal/docker"
    12  	"github.com/tilt-dev/tilt/internal/testutils"
    13  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    14  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    15  )
    16  
    17  // TestVariableInterpolation both ensures Tilt properly passes environment to Compose for interpolation
    18  // as well as catches potential regressions in the upstream YAML parsing from compose-go (currently, a
    19  // fallback mechanism relies on the user having the v1 (Python) docker-compose CLI installed to work
    20  // around bugs in the compose-go library (also used by the v2 CLI, and is susceptible to the same
    21  // issues)
    22  func TestVariableInterpolation(t *testing.T) {
    23  	if testing.Short() {
    24  		// remove this once the fallback to docker-compose CLI for YAML parse is eliminated
    25  		// (dependent upon compose-go upstream bugs being fixed)
    26  		t.Skip("skipping test that invokes docker-compose CLI in short mode")
    27  	}
    28  
    29  	f := newDCFixture(t)
    30  
    31  	output := `services:
    32    app:
    33      command: sh -c 'node app.js'
    34      image: "$DC_TEST_IMAGE"
    35      build:
    36        context: $DC_TEST_CONTEXT
    37        dockerfile: ${DC_TEST_DOCKERFILE}
    38      ports:
    39        - target: $DC_TEST_PORT
    40          published: 8080
    41          protocol: tcp
    42          mode: ingress
    43  `
    44  
    45  	// the value is already quoted - Compose should NOT add extra quotes
    46  	testutils.Setenv(t, "DC_TEST_IMAGE", "myimage")
    47  	// unquoted 0 is a number in YAML, but Compose SHOULD quote this properly for the field string
    48  	// N.B. the path MUST exist or Compose will fail loading!
    49  	testutils.Setenv(t, "DC_TEST_CONTEXT", "0")
    50  	f.tmpdir.MkdirAll("0")
    51  	// unquoted Y is a bool in YAML, but Compose SHOULD quote this properly for the field string
    52  	testutils.Setenv(t, "DC_TEST_DOCKERFILE", "Y")
    53  	// Compose should NOT quote this since the field is numeric
    54  	testutils.Setenv(t, "DC_TEST_PORT", "8081")
    55  
    56  	proj := f.loadProject(output)
    57  	if assert.Len(t, proj.Services, 1) {
    58  		svc := proj.Services[0]
    59  		assert.Equal(t, "myimage", svc.Image)
    60  		if assert.NotNil(t, svc.Build) {
    61  			assert.Equal(t, f.tmpdir.JoinPath("0"), svc.Build.Context)
    62  			// resolved Dockerfile path is relative to the context
    63  			assert.Equal(t, "Y", svc.Build.Dockerfile)
    64  		}
    65  		if assert.Len(t, svc.Ports, 1) {
    66  			assert.Equal(t, 8081, int(svc.Ports[0].Target))
    67  		}
    68  	}
    69  }
    70  
    71  func TestParseComposeVersionOutput(t *testing.T) {
    72  	type tc struct {
    73  		version string
    74  		build   string
    75  		output  []byte
    76  	}
    77  	tcs := []tc{
    78  		{
    79  			version: "v1.4.0",
    80  			output: []byte(`docker-compose version: 1.4.0
    81  docker-py version: 1.3.1
    82  CPython version: 2.7.9
    83  OpenSSL version: OpenSSL 1.0.1e 11 Feb 2013
    84  `),
    85  		},
    86  		{
    87  			version: "v1.29.2",
    88  			build:   "5becea4c",
    89  			output: []byte(`docker-compose version 1.29.2, build 5becea4c
    90  docker-py version: 5.0.0
    91  CPython version: 3.7.10
    92  OpenSSL version: OpenSSL 1.1.0l  10 Sep 2019
    93  `),
    94  		},
    95  		{
    96  			version: "v2.0.0-rc.3",
    97  			output:  []byte("Docker Compose version v2.0.0-rc.3\n"),
    98  		},
    99  		{
   100  			version: "v2.0.0-rc.3",
   101  			build:   "bu1ld-info",
   102  			// NOTE: this format is valid semver but as of v2.0.0, has not been used by Compose but is supported
   103  			output: []byte("Docker Compose version v2.0.0-rc.3+bu1ld-info\n"),
   104  		},
   105  	}
   106  	for _, tc := range tcs {
   107  		name := tc.version
   108  		if tc.build != "" {
   109  			name += "+" + tc.build
   110  		}
   111  		t.Run(name, func(t *testing.T) {
   112  			version, build, err := parseComposeVersionOutput(tc.output)
   113  			require.NoError(t, err)
   114  			require.Equal(t, tc.version, version)
   115  			require.Equal(t, tc.build, build)
   116  		})
   117  	}
   118  }
   119  
   120  func TestLoadEnvFile(t *testing.T) {
   121  	if testing.Short() {
   122  		// remove this once the fallback to docker-compose CLI for YAML parse is eliminated
   123  		// (dependent upon compose-go upstream bugs being fixed)
   124  		t.Skip("skipping test that invokes docker-compose CLI in short mode")
   125  	}
   126  
   127  	f := newDCFixture(t)
   128  	f.tmpdir.WriteFile(".env", "COMMAND=foo")
   129  
   130  	dcYAML := `services:
   131    foo:
   132      command: ${COMMAND}
   133      image: asdf
   134  `
   135  	proj := f.loadProject(dcYAML)
   136  	require.Equal(t, types.ShellCommand{"foo"}, proj.Services[0].Command)
   137  }
   138  
   139  type dcFixture struct {
   140  	t      testing.TB
   141  	ctx    context.Context
   142  	cli    DockerComposeClient
   143  	tmpdir *tempdir.TempDirFixture
   144  }
   145  
   146  func newDCFixture(t testing.TB) *dcFixture {
   147  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   148  
   149  	tmpdir := tempdir.NewTempDirFixture(t)
   150  
   151  	return &dcFixture{
   152  		t:      t,
   153  		ctx:    ctx,
   154  		cli:    NewDockerComposeClient(docker.LocalEnv{}),
   155  		tmpdir: tmpdir,
   156  	}
   157  }
   158  
   159  func (f *dcFixture) loadProject(composeYAML string) *types.Project {
   160  	f.t.Helper()
   161  	f.tmpdir.WriteFile("docker-compose.yaml", composeYAML)
   162  	proj, err := f.cli.Project(f.ctx, v1alpha1.DockerComposeProject{ConfigPaths: []string{f.tmpdir.JoinPath("docker-compose.yaml")}})
   163  	require.NoError(f.t, err, "Failed to parse compose YAML")
   164  	return proj
   165  }