mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
fix(config): resolve service environment when computing --hash
Containers are created from a project whose service environment is resolved (env_file merged into environment), but since v2.22.0 `config --hash` skipped that resolution, so hashes diverged from the com.docker.compose.config-hash label for services using env_file. Resolve the environment of the hashed services only, so a broken env_file or platforms on an unrelated service still doesn't prevent hashing, and honor --no-env-resolution as an escape hatch. Fixes #14001 Signed-off-by: Guillaume Lours <[email protected]>
This commit is contained in:
committed by
Nicolas De loof
parent
5573bd2601
commit
7bc2630b65
+15
-7
@@ -23,7 +23,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -570,18 +569,27 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 err := applyPlatforms(project, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
services = project.ServiceNames()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
sorted := services
|
||||
slices.Sort(sorted)
|
||||
|
||||
for _, name := range sorted {
|
||||
for _, name := range project.ServiceNames() {
|
||||
s, err := project.GetService(name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
EXAMPLE=value
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user