Skip to content

Commit

Permalink
fix(hash): recreate container on project config content change
Browse files Browse the repository at this point in the history
Signed-off-by: Suleiman Dibirov <[email protected]>
  • Loading branch information
idsulik committed Jul 12, 2024
1 parent 11d5ecd commit f199c24
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 15 deletions.
2 changes: 1 addition & 1 deletion cmd/compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
return err
}

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

if err != nil {
return err
Expand Down
10 changes: 5 additions & 5 deletions pkg/compose/convergence.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,

sort.Slice(containers, func(i, j int) bool {
// select obsolete containers first, so they get removed as we scale down
if obsolete, _ := mustRecreate(service, containers[i], recreate); obsolete {
if obsolete, _ := mustRecreate(project, service, containers[i], recreate); obsolete {
// i is obsolete, so must be first in the list
return true
}
if obsolete, _ := mustRecreate(service, containers[j], recreate); obsolete {
if obsolete, _ := mustRecreate(project, service, containers[j], recreate); obsolete {
// j is obsolete, so must be first in the list
return false
}
Expand All @@ -158,7 +158,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
continue
}

mustRecreate, err := mustRecreate(service, container, recreate)
mustRecreate, err := mustRecreate(project, service, container, recreate)
if err != nil {
return err
}
Expand Down Expand Up @@ -292,14 +292,14 @@ func (c *convergence) resolveSharedNamespaces(service *types.ServiceConfig) erro
return nil
}

func mustRecreate(expected types.ServiceConfig, actual moby.Container, policy string) (bool, error) {
func mustRecreate(project *types.Project, expected types.ServiceConfig, actual moby.Container, policy string) (bool, error) {
if policy == api.RecreateNever {
return false, nil
}
if policy == api.RecreateForce || expected.Extensions[extLifecycle] == forceRecreate {
return true, nil
}
configHash, err := ServiceHash(expected)
configHash, err := ServiceHash(project, expected)
if err != nil {
return false, err
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
inherit *moby.Container,
opts createOptions,
) (createConfigs, error) {
labels, err := s.prepareLabels(opts.Labels, service, number)
labels, err := s.prepareLabels(opts.Labels, p, service, number)
if err != nil {
return createConfigs{}, err
}
Expand Down Expand Up @@ -499,8 +499,8 @@ func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, bool,
return parsed, unconfined, nil
}

func (s *composeService) prepareLabels(labels types.Labels, service types.ServiceConfig, number int) (map[string]string, error) {
hash, err := ServiceHash(service)
func (s *composeService) prepareLabels(labels types.Labels, project *types.Project, service types.ServiceConfig, number int) (map[string]string, error) {
hash, err := ServiceHash(project, service)
if err != nil {
return nil, err
}
Expand Down
23 changes: 22 additions & 1 deletion pkg/compose/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ package compose

import (
"encoding/json"
"os"

"github.com/compose-spec/compose-go/v2/types"
"github.com/opencontainers/go-digest"
)

// ServiceHash computes the configuration hash for a service.
func ServiceHash(o types.ServiceConfig) (string, error) {
func ServiceHash(project *types.Project, o types.ServiceConfig) (string, error) {
// remove the Build config when generating the service hash
o.Build = nil
o.PullPolicy = ""
Expand All @@ -37,5 +38,25 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
if err != nil {
return "", err
}

for _, serviceConfig := range o.Configs {
projectConfig, ok := project.Configs[serviceConfig.Source]
if !ok {
continue
}

if projectConfig.Content != "" {
bytes = append(bytes, []byte(projectConfig.Content)...)
} else if projectConfig.File != "" {
content, err := os.ReadFile(projectConfig.File)
if err != nil {
return "", err
}
bytes = append(bytes, content...)
} else if projectConfig.Environment != "" {
bytes = append(bytes, []byte(projectConfig.Environment)...)
}
}

return digest.SHA256.FromBytes(bytes).Encoded(), nil
}
69 changes: 64 additions & 5 deletions pkg/compose/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,80 @@ import (
"gotest.tools/v3/assert"
)

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

func serviceConfig(replicas int) types.ServiceConfig {
func TestServiceHashWithIgnorableValues(t *testing.T) {
hash1, err := ServiceHash(&types.Project{}, serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceHash(&types.Project{}, 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))
assert.NilError(t, err)
hash2, err := ServiceHash(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))
assert.NilError(t, err)
hash2, err := ServiceHash(projectConfig("myConfigSource", "", "b", ""), serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Assert(t, hash1 != hash2)
}

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

func projectConfig(configName, configContent, configEnvironment, configFile string) *types.Project {
return &types.Project{
Configs: types.Configs{
configName: types.ConfigObjConfig{
Content: configContent,
Environment: configEnvironment,
File: configFile,
},
},
}
}

func serviceConfig(buildContext, pullPolicy string, replicas int) types.ServiceConfig {
return types.ServiceConfig{
Scale: &replicas,
Build: &types.BuildConfig{
Context: buildContext,
},
PullPolicy: pullPolicy,
Scale: &replicas,
Deploy: &types.DeployConfig{
Replicas: &replicas,
},
Name: "foo",
Image: "bar",
Configs: []types.ServiceConfigObjConfig{
{
Source: "myConfigSource",
},
},
}
}
1 change: 1 addition & 0 deletions pkg/compose/testdata/config1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is 1 config file
1 change: 1 addition & 0 deletions pkg/compose/testdata/config2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is 2 config file

0 comments on commit f199c24

Please sign in to comment.