From debea8e2dcbe8819cddcfb0a7f5f3ae9cb637f51 Mon Sep 17 00:00:00 2001 From: Nico Grashoff Date: Fri, 30 Aug 2024 18:25:55 +0200 Subject: [PATCH] feat(compose): select services via profiles This commit allows users of the compose module to selectively enable services by using Docker Compose profiles. More about profiles: https://docs.docker.com/compose/profiles --- modules/compose/compose.go | 8 +++ modules/compose/compose_api.go | 17 ++++++ modules/compose/compose_api_test.go | 53 +++++++++++++++++++ modules/compose/compose_builder_test.go | 6 +++ .../testdata/docker-compose-profiles.yml | 25 +++++++++ 5 files changed, 109 insertions(+) create mode 100644 modules/compose/testdata/docker-compose-profiles.yml diff --git a/modules/compose/compose.go b/modules/compose/compose.go index b0f26370cb..c63eb73bb1 100644 --- a/modules/compose/compose.go +++ b/modules/compose/compose.go @@ -32,6 +32,7 @@ type composeStackOptions struct { Paths []string temporaryPaths map[string]bool Logger testcontainers.Logging + Profiles []string } type ComposeStackOption interface { @@ -116,6 +117,11 @@ func WithStackReaders(readers ...io.Reader) ComposeStackOption { return ComposeStackReaders(readers) } +// WithProfiles allows to enable/disable services based on the profiles defined in the compose file. +func WithProfiles(profiles ...string) ComposeStackOption { + return ComposeProfiles(profiles) +} + func NewDockerCompose(filePaths ...string) (*dockerCompose, error) { return NewDockerComposeWith(WithStackFiles(filePaths...)) } @@ -125,6 +131,7 @@ func NewDockerComposeWith(opts ...ComposeStackOption) (*dockerCompose, error) { Identifier: uuid.New().String(), temporaryPaths: make(map[string]bool), Logger: testcontainers.Logger, + Profiles: nil, } for i := range opts { @@ -168,6 +175,7 @@ func NewDockerComposeWith(opts ...ComposeStackOption) (*dockerCompose, error) { configs: composeOptions.Paths, temporaryConfigs: composeOptions.temporaryPaths, logger: composeOptions.Logger, + projectProfiles: composeOptions.Profiles, composeService: compose.NewComposeService(dockerCli), dockerClient: dockerCli.Client(), waitStrategies: make(map[string]wait.Strategy), diff --git a/modules/compose/compose_api.go b/modules/compose/compose_api.go index 129f55897e..d1b18ec3b6 100644 --- a/modules/compose/compose_api.go +++ b/modules/compose/compose_api.go @@ -153,6 +153,13 @@ func (f ComposeStackFiles) applyToComposeStack(o *composeStackOptions) error { return nil } +type ComposeProfiles []string + +func (p ComposeProfiles) applyToComposeStack(o *composeStackOptions) error { + o.Profiles = append(o.Profiles, p...) + return nil +} + type StackIdentifier string func (f StackIdentifier) applyToComposeStack(o *composeStackOptions) error { @@ -212,6 +219,9 @@ type dockerCompose struct { // e.g. environment settings, ... projectOptions []cli.ProjectOptionsFn + // profiles applied to the compose project after compilation. + projectProfiles []string + // compiled compose project // can be nil if the stack wasn't started yet project *types.Project @@ -512,6 +522,13 @@ func (d *dockerCompose) compileProject(ctx context.Context) (*types.Project, err return nil, fmt.Errorf("load project: %w", err) } + if len(d.projectProfiles) > 0 { + proj, err = proj.WithProfiles(d.projectProfiles) + if err != nil { + return nil, fmt.Errorf("with profiles: %w", err) + } + } + for i, s := range proj.Services { s.CustomLabels = map[string]string{ api.ProjectLabel: proj.Name, diff --git a/modules/compose/compose_api_test.go b/modules/compose/compose_api_test.go index 6771454347..7879dabfa9 100644 --- a/modules/compose/compose_api_test.go +++ b/modules/compose/compose_api_test.go @@ -101,6 +101,59 @@ func TestDockerComposeAPIWithRunServices(t *testing.T) { assert.Contains(t, serviceNames, "api-nginx") } +func TestDockerComposeAPIWithProfiles(t *testing.T) { + path := RenderComposeProfiles(t) + + testcases := map[string]struct { + withProfiles []string + wantServices []string + }{ + "nil profile": { + withProfiles: nil, + wantServices: []string{"starts-always"}, + }, + "no profiles": { + withProfiles: []string{}, + wantServices: []string{"starts-always"}, + }, + "dev profile": { + withProfiles: []string{"dev"}, + wantServices: []string{"starts-always", "only-dev", "dev-or-test"}, + }, + "test profile": { + withProfiles: []string{"test"}, + wantServices: []string{"starts-always", "dev-or-test"}, + }, + "wildcard profile": { + withProfiles: []string{"*"}, + wantServices: []string{"starts-always", "only-dev", "dev-or-test", "only-prod"}, + }, + "undefined profile": { + withProfiles: []string{"undefined-profile"}, + wantServices: []string{"starts-always"}, + }, + } + + for name, test := range testcases { + t.Run(name, func(t *testing.T) { + compose, err := NewDockerComposeWith(WithStackFiles(path), WithProfiles(test.withProfiles...)) + require.NoError(t, err, "NewDockerCompose()") + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + for _, service := range test.wantServices { + compose = compose.WaitForService(service, wait.NewHTTPStrategy("/").WithPort("80/tcp").WithStartupTimeout(10*time.Second)).(*dockerCompose) + } + err = compose.Up(ctx, Wait(true)) + cleanup(t, compose) + require.NoError(t, err, "compose.Up()") + + assert.ElementsMatch(t, test.wantServices, compose.Services()) + }) + } +} + func TestDockerComposeAPI_TestcontainersLabelsArePresent(t *testing.T) { path, _ := RenderComposeComplex(t) compose, err := NewDockerCompose(path) diff --git a/modules/compose/compose_builder_test.go b/modules/compose/compose_builder_test.go index fbfe37baa3..4624c1d3c2 100644 --- a/modules/compose/compose_builder_test.go +++ b/modules/compose/compose_builder_test.go @@ -14,6 +14,12 @@ const ( testdataPackage = "testdata" ) +func RenderComposeProfiles(t *testing.T) string { + t.Helper() + + return writeTemplate(t, "docker-compose-profiles.yml") +} + func RenderComposeComplex(t *testing.T) (string, []int) { t.Helper() diff --git a/modules/compose/testdata/docker-compose-profiles.yml b/modules/compose/testdata/docker-compose-profiles.yml new file mode 100644 index 0000000000..fdb92853e1 --- /dev/null +++ b/modules/compose/testdata/docker-compose-profiles.yml @@ -0,0 +1,25 @@ +services: + starts-always: + image: docker.io/nginx:stable-alpine + ports: + - ":80" + # profiles: none defined, therefore always starts. + only-dev: + image: docker.io/nginx:stable-alpine + ports: + - ":80" + profiles: + - dev + dev-or-test: + image: docker.io/nginx:stable-alpine + ports: + - ":80" + profiles: + - dev + - test + only-prod: + image: docker.io/nginx:stable-alpine + ports: + - ":80" + profiles: + - prod