Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions cmd/compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"fmt"
"io"
"os"
"slices"
"sort"
"strings"

Expand Down Expand Up @@ -570,18 +569,27 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
return err
}

if err := applyPlatforms(project, true); err != nil {
// narrow the project to the services being hashed, so a broken env_file
// or platforms on an unrelated service doesn't prevent hashing the requested ones
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
if err != nil {
return err
}

if len(services) == 0 {
services = project.ServiceNames()
if err := applyPlatforms(project, true); err != nil {
return err
}

sorted := services
slices.Sort(sorted)
if !opts.noResolveEnv {
// containers are created from a project with service environment
// resolved (env_file merged into environment), so hash the same content
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
}
}

for _, name := range sorted {
for _, name := range project.ServiceNames() {
s, err := project.GetService(name)
if err != nil {
return err
Expand Down
29 changes: 29 additions & 0 deletions pkg/e2e/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
package e2e

import (
"fmt"
"strings"
"testing"

"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)

Expand Down Expand Up @@ -114,3 +117,29 @@ func TestLocalComposeConfig(t *testing.T) {
res.Assert(t, icmd.Expected{Out: `gated`})
})
}

func TestConfigHashMatchesContainerLabel(t *testing.T) {
c := NewParallelCLI(t)

const projectName = "compose-e2e-config-hash"
defer c.cleanupWithDown(t, projectName)

c.RunDockerComposeCmd(t, "-f", "./fixtures/config_hash/compose.yaml", "--project-name", projectName, "up", "-d", "--force-recreate")

containerHashes := map[string]string{}
for _, service := range []string{"with-env-file", "optional-env", "plain"} {
res := c.RunDockerCmd(t, "inspect", "-f", `{{index .Config.Labels "com.docker.compose.config-hash"}}`,
fmt.Sprintf("%s-%s-1", projectName, service))
containerHashes[service] = strings.TrimSpace(res.Stdout())

res = c.RunDockerComposeCmd(t, "-f", "./fixtures/config_hash/compose.yaml", "--project-name", projectName, "config", "--hash", service)
fields := strings.Fields(res.Stdout())
assert.Equal(t, len(fields), 2)
assert.Equal(t, fields[1], containerHashes[service], "config --hash %s must match the container config-hash label", service)
}

res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config_hash/compose.yaml", "--project-name", projectName, "config", "--hash", "*")
expected := fmt.Sprintf("optional-env %s\nplain %s\nwith-env-file %s",
containerHashes["optional-env"], containerHashes["plain"], containerHashes["with-env-file"])
assert.Equal(t, strings.TrimSpace(res.Stdout()), expected)
}
14 changes: 14 additions & 0 deletions pkg/e2e/env_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ func TestUnusedMissingEnvFile(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/env_file/compose.yaml", "scale", "serviceB=1")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "env file /doesnotexist/.env not found"})

// config --hash should work for services not referencing the missing env file
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "config", "--hash", "serviceA")

// but hashing the service with the missing env file must fail, as up would
res = c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/env_file/compose.yaml", "config", "--hash", "serviceB")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "env file /doesnotexist/.env not found"})

// unless env_file resolution is explicitly disabled
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "config", "--no-env-resolution", "--hash", "serviceB")

// and so must the wildcard, as it includes serviceB
res = c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/env_file/compose.yaml", "config", "--hash", "*")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "env file /doesnotexist/.env not found"})

// shell completion should list services even with missing env file.
// ComposeStandalonePath fails the test outside standalone mode, so only the
// plugin form can be the default here.
Expand Down
1 change: 1 addition & 0 deletions pkg/e2e/fixtures/config_hash/app.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EXAMPLE=value
17 changes: 17 additions & 0 deletions pkg/e2e/fixtures/config_hash/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
with-env-file:
image: alpine
command: ["sleep", "infinity"]
env_file:
- app.env

optional-env:
image: alpine
command: ["sleep", "infinity"]
env_file:
- path: missing-optional.env
required: false

plain:
image: alpine
command: ["sleep", "infinity"]
Loading