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
fixes
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
  • Loading branch information
idsulik committed Feb 24, 2025
commit f97276d27aff7843c3c29bbed5b6245001ca6381
4 changes: 2 additions & 2 deletions pkg/api/labels.go
Original file line number Diff line number Diff line change
@@ -32,9 +32,9 @@ const (
// ConfigHashLabel stores configuration hash for a compose service
ConfigHashLabel = "com.docker.compose.config-hash"
// ServiceConfigsHash stores configuration hash for a compose service configs
ServiceConfigsHash = "com.docker.compose.service.configs-hash"
ServiceConfigsHash = "com.docker.compose.service.%s.configs.hash"
// ServiceSecretsHash stores configuration hash for a compose service secrets
ServiceSecretsHash = "com.docker.compose.service.secrets-hash"
ServiceSecretsHash = "com.docker.compose.service.%s.secrets.hash"
// 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
19 changes: 13 additions & 6 deletions pkg/compose/convergence.go
Original file line number Diff line number Diff line change
@@ -323,7 +323,8 @@ func (c *convergence) resolveSharedNamespaces(service *types.ServiceConfig) erro
return nil
}

func (c *convergence) mustRecreate(project *types.Project,, expected types.ServiceConfig, actual containerType.Summary, policy string) (bool, error) {
//nolint:gocyclo
func (c *convergence) mustRecreate(project *types.Project, expected types.ServiceConfig, actual containerType.Summary, policy string) (bool, error) {
if policy == api.RecreateNever {
return false, nil
}
@@ -343,20 +344,26 @@ func (c *convergence) mustRecreate(project *types.Project,, expected types.Servi
return true, nil
}

serviceConfigsHash, err := ServiceConfigsHash(project, expected)
serviceNameToConfigHash, err := ServiceConfigsHash(project, expected)
if err != nil {
return false, err
}

for serviceName, hash := range serviceNameToConfigHash {
if actual.Labels[fmt.Sprintf(api.ServiceConfigsHash, serviceName)] != hash {
return true, nil
}
}

serviceSecretsHash, err := ServiceSecretsHash(project, expected)
if err != nil {
return false, err
}
serviceConfigsChanged := actual.Labels[api.ServiceConfigsHash] != serviceConfigsHash
serviceSecretsChanged := actual.Labels[api.ServiceSecretsHash] != serviceSecretsHash

if serviceConfigsChanged || serviceSecretsChanged {
return true, nil
for serviceName, hash := range serviceSecretsHash {
if actual.Labels[fmt.Sprintf(api.ServiceSecretsHash, serviceName)] != hash {
return true, nil
}
}

if c.networks != nil && actual.State == "running" {
14 changes: 10 additions & 4 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
@@ -513,23 +513,29 @@ func (s *composeService) prepareLabels(labels types.Labels, project *types.Proje
return nil, err
}

serviceConfigsHash, err := ServiceConfigsHash(project, service)
serviceNameToConfigHash, err := ServiceConfigsHash(project, service)
if err != nil {
return nil, err
}

serviceSecretsHash, err := ServiceSecretsHash(project, service)
for serviceName, hash := range serviceNameToConfigHash {
labels[fmt.Sprintf(api.ServiceConfigsHash, serviceName)] = hash
}

serviceNameToSecretHash, err := ServiceSecretsHash(project, service)
if err != nil {
return nil, err
}

for serviceName, hash := range serviceNameToSecretHash {
labels[fmt.Sprintf(api.ServiceSecretsHash, serviceName)] = hash
}

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

var dependencies []string
20 changes: 10 additions & 10 deletions pkg/compose/hash.go
Original file line number Diff line number Diff line change
@@ -47,37 +47,37 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
}

// ServiceConfigsHash computes the configuration hash for service configs.
func ServiceConfigsHash(project *types.Project, serviceConfig types.ServiceConfig) (string, error) {
data := make([]byte, 0)
func ServiceConfigsHash(project *types.Project, serviceConfig types.ServiceConfig) (map[string]string, error) {
serviceNameToHash := make(map[string]string)
for _, config := range serviceConfig.Configs {
file := project.Configs[config.Source]
b, err := createTarForConfig(project, types.FileReferenceConfig(config), types.FileObjectConfig(file))

if err != nil {
return "", err
return nil, err
}

data = append(data, b.Bytes()...)
serviceNameToHash[config.Target] = digest.SHA256.FromBytes(b.Bytes()).Encoded()
}

return digest.SHA256.FromBytes(data).Encoded(), nil
return serviceNameToHash, nil
}

// ServiceSecretsHash computes the configuration hash for service secrets.
func ServiceSecretsHash(project *types.Project, serviceConfig types.ServiceConfig) (string, error) {
data := make([]byte, 0)
func ServiceSecretsHash(project *types.Project, serviceConfig types.ServiceConfig) (map[string]string, error) {
serviceNameToHash := make(map[string]string)
for _, secret := range serviceConfig.Secrets {
file := project.Secrets[secret.Source]
b, err := createTarForConfig(project, types.FileReferenceConfig(secret), types.FileObjectConfig(file))

if err != nil {
return "", err
return nil, err
}

data = append(data, b.Bytes()...)
serviceNameToHash[secret.Target] = digest.SHA256.FromBytes(b.Bytes()).Encoded()
}

return digest.SHA256.FromBytes(data).Encoded(), nil
return serviceNameToHash, nil
}

func createTarForConfig(
80 changes: 56 additions & 24 deletions pkg/compose/hash_test.go
Original file line number Diff line number Diff line change
@@ -40,79 +40,111 @@ func TestServiceHashWithIgnorableValues(t *testing.T) {
}

func TestServiceConfigsHashWithoutChangesContent(t *testing.T) {
hash1, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext1", "always", 1))
serviceNameToConfigHash1, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext2", "never", 2))
serviceNameToConfigHas2, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 == hash2)
assert.Equal(t, len(serviceNameToConfigHash1), len(serviceNameToConfigHas2))

for serviceName, hash := range serviceNameToConfigHash1 {
assert.Equal(t, hash, serviceNameToConfigHas2[serviceName])
}
}

func TestServiceConfigsHashWithChangedConfigContent(t *testing.T) {
hash1, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext1", "always", 1))
serviceNameToConfigHash1, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceConfigsHash(projectWithConfigs("b", "", ""), serviceConfig("myContext2", "never", 2))
serviceNameToConfigHash2, err := ServiceConfigsHash(projectWithConfigs("b", "", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
assert.Equal(t, len(serviceNameToConfigHash1), len(serviceNameToConfigHash2))

for serviceName, hash := range serviceNameToConfigHash1 {
assert.Assert(t, hash != serviceNameToConfigHash2[serviceName])
}
}

func TestServiceConfigsHashWithChangedConfigEnvironment(t *testing.T) {
hash1, err := ServiceConfigsHash(projectWithConfigs("", "a", ""), serviceConfig("myContext1", "always", 1))
serviceNameToConfigHash1, err := ServiceConfigsHash(projectWithConfigs("", "a", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceConfigsHash(projectWithConfigs("", "b", ""), serviceConfig("myContext2", "never", 2))
serviceNameToConfigHash2, err := ServiceConfigsHash(projectWithConfigs("", "b", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
assert.Equal(t, len(serviceNameToConfigHash1), len(serviceNameToConfigHash2))

for serviceName, hash := range serviceNameToConfigHash1 {
assert.Assert(t, hash != serviceNameToConfigHash2[serviceName])
}
}

func TestServiceConfigsHashWithChangedConfigFile(t *testing.T) {
hash1, err := ServiceConfigsHash(
serviceNameToConfigHash1, err := ServiceConfigsHash(
projectWithConfigs("", "", "./testdata/config1.txt"),
serviceConfig("myContext1", "always", 1),
)
assert.NilError(t, err)
hash2, err := ServiceConfigsHash(
serviceNameToConfigHash2, err := ServiceConfigsHash(
projectWithConfigs("", "", "./testdata/config2.txt"),
serviceConfig("myContext2", "never", 2),
)
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
assert.Equal(t, len(serviceNameToConfigHash1), len(serviceNameToConfigHash2))

for serviceName, hash := range serviceNameToConfigHash1 {
assert.Assert(t, hash != serviceNameToConfigHash2[serviceName])
}
}

func TestServiceSecretsHashWithoutChangesContent(t *testing.T) {
hash1, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext1", "always", 1))
serviceNameToSecretHash1, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext2", "never", 2))
serviceNameToSecretHash2, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 == hash2)
assert.Equal(t, len(serviceNameToSecretHash1), len(serviceNameToSecretHash2))

for serviceName, hash := range serviceNameToSecretHash1 {
assert.Equal(t, hash, serviceNameToSecretHash2[serviceName])
}
}

func TestServiceSecretsHashWithChangedSecretContent(t *testing.T) {
hash1, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext1", "always", 1))
serviceNameToSecretHash1, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceSecretsHash(projectWithSecrets("b", "", ""), serviceConfig("myContext2", "never", 2))
serviceNameToSecretHash2, err := ServiceSecretsHash(projectWithSecrets("b", "", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
assert.Equal(t, len(serviceNameToSecretHash1), len(serviceNameToSecretHash2))

for serviceName, hash := range serviceNameToSecretHash1 {
assert.Assert(t, hash != serviceNameToSecretHash2[serviceName])
}
}

func TestServiceSecretsHashWithChangedSecretEnvironment(t *testing.T) {
hash1, err := ServiceSecretsHash(projectWithSecrets("", "a", ""), serviceConfig("myContext1", "always", 1))
serviceNameToSecretHash1, err := ServiceSecretsHash(projectWithSecrets("", "a", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceSecretsHash(projectWithSecrets("", "b", ""), serviceConfig("myContext2", "never", 2))
serviceNameToSecretHash2, err := ServiceSecretsHash(projectWithSecrets("", "b", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
assert.Equal(t, len(serviceNameToSecretHash1), len(serviceNameToSecretHash2))

for serviceName, hash := range serviceNameToSecretHash1 {
assert.Assert(t, hash != serviceNameToSecretHash2[serviceName])
}
}

func TestServiceSecretsHashWithChangedSecretFile(t *testing.T) {
hash1, err := ServiceSecretsHash(
serviceNameToSecretHash1, err := ServiceSecretsHash(
projectWithSecrets("", "", "./testdata/config1.txt"),
serviceConfig("myContext1", "always", 1),
)
assert.NilError(t, err)
hash2, err := ServiceSecretsHash(
serviceNameToSecretHash2, err := ServiceSecretsHash(
projectWithSecrets("", "", "./testdata/config2.txt"),
serviceConfig("myContext2", "never", 2),
)
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
assert.Equal(t, len(serviceNameToSecretHash1), len(serviceNameToSecretHash2))

for serviceName, hash := range serviceNameToSecretHash1 {
assert.Assert(t, hash != serviceNameToSecretHash2[serviceName])
}
}

func projectWithConfigs(configContent, configEnvironmentValue, configFile string) *types.Project {