Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(hash): recreate container on project config content change #11931

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Revert "fix(hash): recreate container on project config content change"
This reverts commit 64c37bf.

Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
  • Loading branch information
idsulik committed Feb 24, 2025
commit 0056225b015790a920fd44d5b80a4449e2d9deb1
2 changes: 1 addition & 1 deletion cmd/compose/config.go
Original file line number Diff line number Diff line change
@@ -347,7 +347,7 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
return err
}

hash, err := compose.ServiceHash(project, s)
hash, err := compose.ServiceHash(s)

if err != nil {
return err
2 changes: 2 additions & 0 deletions pkg/api/labels.go
Original file line number Diff line number Diff line change
@@ -31,6 +31,8 @@ const (
ServiceLabel = "com.docker.compose.service"
// ConfigHashLabel stores configuration hash for a compose service
ConfigHashLabel = "com.docker.compose.config-hash"
// ConfigHashDependenciesLabel stores configuration hash for a compose service dependencies
ConfigHashDependenciesLabel = "com.docker.compose.config-hash-dependencies"
// ContainerNumberLabel stores the container index of a replicated service
ContainerNumberLabel = "com.docker.compose.container-number"
// VolumeLabel allow to track resource related to a compose volume
15 changes: 14 additions & 1 deletion pkg/compose/convergence.go
Original file line number Diff line number Diff line change
@@ -330,7 +330,20 @@ func (c *convergence) mustRecreate(project *types.Project,, expected types.Servi
if policy == api.RecreateForce {
return true, nil
}
configHash, err := ServiceHash(project, expected)
serviceHash, err := ServiceHash(expected)
if err != nil {
return false, err
}

if actual.Labels[api.ConfigHashLabel] != serviceHash {
return true, nil
}

if actual.Labels[api.ImageDigestLabel] != expected.CustomLabels[api.ImageDigestLabel] {
return true, nil
}

serviceDependenciesHash, err := ServiceDependenciesHash(project, expected)
if err != nil {
return false, err
}
11 changes: 9 additions & 2 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
@@ -508,16 +508,23 @@ func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, bool,
}

func (s *composeService) prepareLabels(labels types.Labels, project *types.Project, service types.ServiceConfig, number int) (map[string]string, error) {
hash, err := ServiceHash(project, service)
serviceHash, err := ServiceHash(service)
if err != nil {
return nil, err
}

serviceDependenciesHash, err := ServiceDependenciesHash(project, service)
if err != nil {
return nil, err
}
labels[api.ConfigHashLabel] = hash

if number > 0 {
// One-off containers are not indexed
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
}
labels[api.ConfigHashLabel] = serviceHash
labels[api.ConfigHashDependenciesLabel] = serviceDependenciesHash
labels[api.ContainerNumberLabel] = strconv.Itoa(number)

var dependencies []string
for s, d := range service.DependsOn {
7 changes: 6 additions & 1 deletion pkg/compose/hash.go
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ import (
)

// ServiceHash computes the configuration hash for a service.
func ServiceHash(project *types.Project, o types.ServiceConfig) (string, error) {
func ServiceHash(o types.ServiceConfig) (string, error) {
// remove the Build config when generating the service hash
o.Build = nil
o.PullPolicy = ""
@@ -40,7 +40,12 @@ func ServiceHash(project *types.Project, o types.ServiceConfig) (string, error)
if err != nil {
return "", err
}
return digest.SHA256.FromBytes(bytes).Encoded(), nil
}

// ServiceDependenciesHash computes the configuration hash for service dependencies.
func ServiceDependenciesHash(project *types.Project, o types.ServiceConfig) (string, error) {
bytes := make([]byte, 0)
for _, serviceConfig := range o.Configs {
projectConfig, ok := project.Configs[serviceConfig.Source]
if !ok {
34 changes: 21 additions & 13 deletions pkg/compose/hash_test.go
Original file line number Diff line number Diff line change
@@ -24,44 +24,52 @@ import (
)

func TestServiceHashWithAllValuesTheSame(t *testing.T) {
hash1, err := ServiceHash(projectConfig("a", "b", "c", ""), serviceConfig("myContext1", "always", 1))
hash1, err := ServiceHash(serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceHash(projectConfig("a", "b", "c", ""), serviceConfig("myContext1", "always", 1))
hash2, err := ServiceHash(serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
assert.Equal(t, hash1, hash2)
}

func TestServiceHashWithIgnorableValues(t *testing.T) {
hash1, err := ServiceHash(&types.Project{}, serviceConfig("myContext1", "always", 1))
hash1, err := ServiceHash(serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceHash(&types.Project{}, serviceConfig("myContext2", "never", 2))
hash2, err := ServiceHash(serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Equal(t, hash1, hash2)
}

func TestServiceHashWithChangedConfigContent(t *testing.T) {
hash1, err := ServiceHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext1", "always", 1))
func TestServiceDependenciesHashWithoutChangesContent(t *testing.T) {
hash1, err := ServiceDependenciesHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceHash(projectConfig("myConfigSource", "b", "", ""), serviceConfig("myContext2", "never", 2))
hash2, err := ServiceDependenciesHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 == hash2)
}

func TestServiceDependenciesHashWithChangedConfigContent(t *testing.T) {
hash1, err := ServiceDependenciesHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceDependenciesHash(projectConfig("myConfigSource", "b", "", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
}

func TestServiceHashWithChangedConfigEnvironment(t *testing.T) {
hash1, err := ServiceHash(projectConfig("myConfigSource", "", "a", ""), serviceConfig("myContext1", "always", 1))
func TestServiceDependenciesHashWithChangedConfigEnvironment(t *testing.T) {
hash1, err := ServiceDependenciesHash(projectConfig("myConfigSource", "", "a", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceHash(projectConfig("myConfigSource", "", "b", ""), serviceConfig("myContext2", "never", 2))
hash2, err := ServiceDependenciesHash(projectConfig("myConfigSource", "", "b", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
}

func TestServiceHashWithChangedConfigFile(t *testing.T) {
hash1, err := ServiceHash(
func TestServiceDependenciesHashWithChangedConfigFile(t *testing.T) {
hash1, err := ServiceDependenciesHash(
projectConfig("myConfigSource", "", "", "./testdata/config1.txt"),
serviceConfig("myContext1", "always", 1),
)
assert.NilError(t, err)
hash2, err := ServiceHash(
hash2, err := ServiceDependenciesHash(
projectConfig("myConfigSource", "", "", "./testdata/config2.txt"),
serviceConfig("myContext2", "never", 2),
)