From f199c2435c17e82257f7d3d8417c3c1cd6c18a45 Mon Sep 17 00:00:00 2001 From: Suleiman Dibirov Date: Sun, 23 Jun 2024 19:10:20 +0300 Subject: [PATCH] fix(hash): recreate container on project config content change Signed-off-by: Suleiman Dibirov --- cmd/compose/config.go | 2 +- pkg/compose/convergence.go | 10 ++--- pkg/compose/create.go | 6 +-- pkg/compose/hash.go | 23 ++++++++++- pkg/compose/hash_test.go | 69 +++++++++++++++++++++++++++++--- pkg/compose/testdata/config1.txt | 1 + pkg/compose/testdata/config2.txt | 1 + 7 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 pkg/compose/testdata/config1.txt create mode 100644 pkg/compose/testdata/config2.txt diff --git a/cmd/compose/config.go b/cmd/compose/config.go index 8325946cdbc..b6b1f352d81 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -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 diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 3b680fe77c3..82b58e379f8 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -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 } @@ -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 } @@ -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 } diff --git a/pkg/compose/create.go b/pkg/compose/create.go index ccf92058057..92a2b0d3a6b 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -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 } @@ -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 } diff --git a/pkg/compose/hash.go b/pkg/compose/hash.go index 284ccaa76fc..b18232232f0 100644 --- a/pkg/compose/hash.go +++ b/pkg/compose/hash.go @@ -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 = "" @@ -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 } diff --git a/pkg/compose/hash_test.go b/pkg/compose/hash_test.go index 73b7f387735..0ae121cd4e4 100644 --- a/pkg/compose/hash_test.go +++ b/pkg/compose/hash_test.go @@ -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", + }, + }, } } diff --git a/pkg/compose/testdata/config1.txt b/pkg/compose/testdata/config1.txt new file mode 100644 index 00000000000..c8a618fe3ee --- /dev/null +++ b/pkg/compose/testdata/config1.txt @@ -0,0 +1 @@ +This is 1 config file diff --git a/pkg/compose/testdata/config2.txt b/pkg/compose/testdata/config2.txt new file mode 100644 index 00000000000..51f7bc363de --- /dev/null +++ b/pkg/compose/testdata/config2.txt @@ -0,0 +1 @@ +This is 2 config file