Skip to content

Commit

Permalink
feat: Add service dependencies hash
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 b79cb5e commit 17237d4
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 17 deletions.
2 changes: 2 additions & 0 deletions pkg/api/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 22 additions & 8 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,20 +292,34 @@ 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)
serviceHash, err := ServiceHash(expected)
if err != nil {
return false, err
}
configChanged := actual.Labels[api.ConfigHashLabel] != configHash
imageUpdated := actual.Labels[api.ImageDigestLabel] != expected.CustomLabels[api.ImageDigestLabel]
return configChanged || imageUpdated, nil

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
}

serviceDependenciesChanged := actual.Labels[api.ConfigHashDependenciesLabel] != serviceDependenciesHash

return serviceDependenciesChanged, nil
}

func getContainerName(projectName string, service types.ServiceConfig, number int) string {
Expand Down
14 changes: 10 additions & 4 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,13 +499,19 @@ 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) {
serviceHash, err := ServiceHash(service)
if err != nil {
return nil, err
}
labels[api.ConfigHashLabel] = hash

serviceDependenciesHash, err := ServiceDependenciesHash(project, service)
if err != nil {
return nil, err
}

labels[api.ConfigHashLabel] = serviceHash
labels[api.ConfigHashDependenciesLabel] = serviceDependenciesHash
labels[api.ContainerNumberLabel] = strconv.Itoa(number)

var dependencies []string
Expand Down
26 changes: 26 additions & 0 deletions pkg/compose/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package compose

import (
"encoding/json"
"os"

"github.com/compose-spec/compose-go/v2/types"
"github.com/opencontainers/go-digest"
Expand All @@ -39,3 +40,28 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
}
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 {
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
}
77 changes: 72 additions & 5 deletions pkg/compose/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,88 @@ import (
"gotest.tools/v3/assert"
)

func TestServiceHash(t *testing.T) {
hash1, err := ServiceHash(serviceConfig(1))
func TestServiceHashWithAllValuesTheSame(t *testing.T) {
hash1, err := ServiceHash(serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceHash(serviceConfig(2))
hash2, err := ServiceHash(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(serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
hash2, err := ServiceHash(serviceConfig("myContext2", "never", 2))
assert.NilError(t, err)
assert.Equal(t, hash1, hash2)
}

func TestServiceDependenciesHashWithoutChangesContent(t *testing.T) {
hash1, err := ServiceDependenciesHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext1", "always", 1))
assert.NilError(t, err)
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 TestServiceDependenciesHashWithChangedConfigEnvironment(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 TestServiceDependenciesHashWithChangedConfigFile(t *testing.T) {
hash1, err := ServiceDependenciesHash(
projectConfig("myConfigSource", "", "", "./testdata/config1.txt"),
serviceConfig("myContext1", "always", 1),
)
assert.NilError(t, err)
hash2, err := ServiceDependenciesHash(
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 17237d4

Please sign in to comment.