Skip to content

Commit fe61eaf

Browse files
committed
feat: add separate hashes for service configs and secrets, add file folder support
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
1 parent 0056225 commit fe61eaf

File tree

7 files changed

+312
-96
lines changed

7 files changed

+312
-96
lines changed

pkg/api/labels.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ const (
3131
ServiceLabel = "com.docker.compose.service"
3232
// ConfigHashLabel stores configuration hash for a compose service
3333
ConfigHashLabel = "com.docker.compose.config-hash"
34-
// ConfigHashDependenciesLabel stores configuration hash for a compose service dependencies
35-
ConfigHashDependenciesLabel = "com.docker.compose.config-hash-dependencies"
34+
// ServiceConfigsHash stores configuration hash for a compose service configs
35+
ServiceConfigsHash = "com.docker.compose.service.configs-hash"
36+
// ServiceSecretsHash stores configuration hash for a compose service secrets
37+
ServiceSecretsHash = "com.docker.compose.service.secrets-hash"
3638
// ContainerNumberLabel stores the container index of a replicated service
3739
ContainerNumberLabel = "com.docker.compose.container-number"
3840
// VolumeLabel allow to track resource related to a compose volume

pkg/compose/convergence.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,13 +343,19 @@ func (c *convergence) mustRecreate(project *types.Project,, expected types.Servi
343343
return true, nil
344344
}
345345

346-
serviceDependenciesHash, err := ServiceDependenciesHash(project, expected)
346+
serviceConfigsHash, err := ServiceConfigsHash(project, expected)
347347
if err != nil {
348348
return false, err
349349
}
350-
configChanged := actual.Labels[api.ConfigHashLabel] != configHash
351-
imageUpdated := actual.Labels[api.ImageDigestLabel] != expected.CustomLabels[api.ImageDigestLabel]
352-
if configChanged || imageUpdated {
350+
351+
serviceSecretsHash, err := ServiceSecretsHash(project, expected)
352+
if err != nil {
353+
return false, err
354+
}
355+
serviceConfigsChanged := actual.Labels[api.ServiceConfigsHash] != serviceConfigsHash
356+
serviceSecretsChanged := actual.Labels[api.ServiceSecretsHash] != serviceSecretsHash
357+
358+
if serviceConfigsChanged || serviceSecretsChanged {
353359
return true, nil
354360
}
355361

pkg/compose/create.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,12 @@ func (s *composeService) prepareLabels(labels types.Labels, project *types.Proje
513513
return nil, err
514514
}
515515

516-
serviceDependenciesHash, err := ServiceDependenciesHash(project, service)
516+
serviceConfigsHash, err := ServiceConfigsHash(project, service)
517+
if err != nil {
518+
return nil, err
519+
}
520+
521+
serviceSecretsHash, err := ServiceSecretsHash(project, service)
517522
if err != nil {
518523
return nil, err
519524
}
@@ -523,7 +528,8 @@ func (s *composeService) prepareLabels(labels types.Labels, project *types.Proje
523528
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
524529
}
525530
labels[api.ConfigHashLabel] = serviceHash
526-
labels[api.ConfigHashDependenciesLabel] = serviceDependenciesHash
531+
labels[api.ServiceConfigsHash] = serviceConfigsHash
532+
labels[api.ServiceSecretsHash] = serviceSecretsHash
527533
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
528534

529535
var dependencies []string

pkg/compose/hash.go

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package compose
1818

1919
import (
20+
"bytes"
2021
"encoding/json"
21-
"os"
22+
"fmt"
23+
"time"
2224

2325
"github.com/compose-spec/compose-go/v2/types"
26+
"github.com/docker/compose/v2/pkg/utils"
2427
"github.com/opencontainers/go-digest"
2528
)
2629

@@ -36,36 +39,77 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
3639
o.DependsOn = nil
3740
o.Profiles = nil
3841

39-
bytes, err := json.Marshal(o)
42+
data, err := json.Marshal(o)
4043
if err != nil {
4144
return "", err
4245
}
43-
return digest.SHA256.FromBytes(bytes).Encoded(), nil
46+
return digest.SHA256.FromBytes(data).Encoded(), nil
4447
}
4548

46-
// ServiceDependenciesHash computes the configuration hash for service dependencies.
47-
func ServiceDependenciesHash(project *types.Project, o types.ServiceConfig) (string, error) {
48-
bytes := make([]byte, 0)
49-
for _, serviceConfig := range o.Configs {
50-
projectConfig, ok := project.Configs[serviceConfig.Source]
51-
if !ok {
52-
continue
49+
// ServiceConfigsHash computes the configuration hash for service configs.
50+
func ServiceConfigsHash(project *types.Project, serviceConfig types.ServiceConfig) (string, error) {
51+
data := make([]byte, 0)
52+
for _, config := range serviceConfig.Configs {
53+
file := project.Configs[config.Source]
54+
b, err := createTarForConfig(project, types.FileReferenceConfig(config), types.FileObjectConfig(file))
55+
56+
if err != nil {
57+
return "", err
58+
}
59+
60+
data = append(data, b.Bytes()...)
61+
}
62+
63+
return digest.SHA256.FromBytes(data).Encoded(), nil
64+
}
65+
66+
// ServiceSecretsHash computes the configuration hash for service secrets.
67+
func ServiceSecretsHash(project *types.Project, serviceConfig types.ServiceConfig) (string, error) {
68+
data := make([]byte, 0)
69+
for _, secret := range serviceConfig.Secrets {
70+
file := project.Secrets[secret.Source]
71+
b, err := createTarForConfig(project, types.FileReferenceConfig(secret), types.FileObjectConfig(file))
72+
73+
if err != nil {
74+
return "", err
5375
}
5476

55-
if projectConfig.Content != "" {
56-
bytes = append(bytes, []byte(projectConfig.Content)...)
57-
} else if projectConfig.File != "" {
58-
content, err := os.ReadFile(projectConfig.File)
59-
if err != nil {
60-
return "", err
61-
}
62-
bytes = append(bytes, content...)
63-
} else if projectConfig.Environment != "" {
64-
bytes = append(bytes, []byte(projectConfig.Environment)...)
77+
data = append(data, b.Bytes()...)
78+
}
79+
80+
return digest.SHA256.FromBytes(data).Encoded(), nil
81+
}
82+
83+
func createTarForConfig(
84+
project *types.Project,
85+
serviceConfig types.FileReferenceConfig,
86+
file types.FileObjectConfig,
87+
) (*bytes.Buffer, error) {
88+
// fixed time to ensure the tarball is deterministic
89+
modTime := time.Unix(0, 0)
90+
91+
if serviceConfig.Target == "" {
92+
serviceConfig.Target = "/" + serviceConfig.Source
93+
}
94+
95+
switch {
96+
case file.Content != "":
97+
return bytes.NewBuffer([]byte(file.Content)), nil
98+
case file.Environment != "":
99+
env, ok := project.Environment[file.Environment]
100+
if !ok {
101+
return nil, fmt.Errorf(
102+
"environment variable %q required by file %q is not set",
103+
file.Environment,
104+
file.Name,
105+
)
65106
}
107+
return bytes.NewBuffer([]byte(env)), nil
108+
case file.File != "":
109+
return utils.CreateTarByPath(file.File, modTime)
66110
}
67111

68-
return digest.SHA256.FromBytes(bytes).Encoded(), nil
112+
return nil, fmt.Errorf("config %q is empty", file.Name)
69113
}
70114

71115
// NetworkHash computes the configuration hash for a network.

pkg/compose/hash_test.go

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,56 +39,124 @@ func TestServiceHashWithIgnorableValues(t *testing.T) {
3939
assert.Equal(t, hash1, hash2)
4040
}
4141

42-
func TestServiceDependenciesHashWithoutChangesContent(t *testing.T) {
43-
hash1, err := ServiceDependenciesHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext1", "always", 1))
42+
func TestServiceConfigsHashWithoutChangesContent(t *testing.T) {
43+
hash1, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext1", "always", 1))
4444
assert.NilError(t, err)
45-
hash2, err := ServiceDependenciesHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext2", "never", 2))
45+
hash2, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext2", "never", 2))
4646
assert.NilError(t, err)
4747
assert.Assert(t, hash1 == hash2)
4848
}
4949

50-
func TestServiceDependenciesHashWithChangedConfigContent(t *testing.T) {
51-
hash1, err := ServiceDependenciesHash(projectConfig("myConfigSource", "a", "", ""), serviceConfig("myContext1", "always", 1))
50+
func TestServiceConfigsHashWithChangedConfigContent(t *testing.T) {
51+
hash1, err := ServiceConfigsHash(projectWithConfigs("a", "", ""), serviceConfig("myContext1", "always", 1))
5252
assert.NilError(t, err)
53-
hash2, err := ServiceDependenciesHash(projectConfig("myConfigSource", "b", "", ""), serviceConfig("myContext2", "never", 2))
53+
hash2, err := ServiceConfigsHash(projectWithConfigs("b", "", ""), serviceConfig("myContext2", "never", 2))
5454
assert.NilError(t, err)
5555
assert.Assert(t, hash1 != hash2)
5656
}
5757

58-
func TestServiceDependenciesHashWithChangedConfigEnvironment(t *testing.T) {
59-
hash1, err := ServiceDependenciesHash(projectConfig("myConfigSource", "", "a", ""), serviceConfig("myContext1", "always", 1))
58+
func TestServiceConfigsHashWithChangedConfigEnvironment(t *testing.T) {
59+
hash1, err := ServiceConfigsHash(projectWithConfigs("", "a", ""), serviceConfig("myContext1", "always", 1))
6060
assert.NilError(t, err)
61-
hash2, err := ServiceDependenciesHash(projectConfig("myConfigSource", "", "b", ""), serviceConfig("myContext2", "never", 2))
61+
hash2, err := ServiceConfigsHash(projectWithConfigs("", "b", ""), serviceConfig("myContext2", "never", 2))
6262
assert.NilError(t, err)
6363
assert.Assert(t, hash1 != hash2)
6464
}
6565

66-
func TestServiceDependenciesHashWithChangedConfigFile(t *testing.T) {
67-
hash1, err := ServiceDependenciesHash(
68-
projectConfig("myConfigSource", "", "", "./testdata/config1.txt"),
66+
func TestServiceConfigsHashWithChangedConfigFile(t *testing.T) {
67+
hash1, err := ServiceConfigsHash(
68+
projectWithConfigs("", "", "./testdata/config1.txt"),
6969
serviceConfig("myContext1", "always", 1),
7070
)
7171
assert.NilError(t, err)
72-
hash2, err := ServiceDependenciesHash(
73-
projectConfig("myConfigSource", "", "", "./testdata/config2.txt"),
72+
hash2, err := ServiceConfigsHash(
73+
projectWithConfigs("", "", "./testdata/config2.txt"),
7474
serviceConfig("myContext2", "never", 2),
7575
)
7676
assert.NilError(t, err)
7777
assert.Assert(t, hash1 != hash2)
7878
}
7979

80-
func projectConfig(configName, configContent, configEnvironment, configFile string) *types.Project {
80+
func TestServiceSecretsHashWithoutChangesContent(t *testing.T) {
81+
hash1, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext1", "always", 1))
82+
assert.NilError(t, err)
83+
hash2, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext2", "never", 2))
84+
assert.NilError(t, err)
85+
assert.Assert(t, hash1 == hash2)
86+
}
87+
88+
func TestServiceSecretsHashWithChangedSecretContent(t *testing.T) {
89+
hash1, err := ServiceSecretsHash(projectWithSecrets("a", "", ""), serviceConfig("myContext1", "always", 1))
90+
assert.NilError(t, err)
91+
hash2, err := ServiceSecretsHash(projectWithSecrets("b", "", ""), serviceConfig("myContext2", "never", 2))
92+
assert.NilError(t, err)
93+
assert.Assert(t, hash1 != hash2)
94+
}
95+
96+
func TestServiceSecretsHashWithChangedSecretEnvironment(t *testing.T) {
97+
hash1, err := ServiceSecretsHash(projectWithSecrets("", "a", ""), serviceConfig("myContext1", "always", 1))
98+
assert.NilError(t, err)
99+
hash2, err := ServiceSecretsHash(projectWithSecrets("", "b", ""), serviceConfig("myContext2", "never", 2))
100+
assert.NilError(t, err)
101+
assert.Assert(t, hash1 != hash2)
102+
}
103+
104+
func TestServiceSecretsHashWithChangedSecretFile(t *testing.T) {
105+
hash1, err := ServiceSecretsHash(
106+
projectWithSecrets("", "", "./testdata/config1.txt"),
107+
serviceConfig("myContext1", "always", 1),
108+
)
109+
assert.NilError(t, err)
110+
hash2, err := ServiceSecretsHash(
111+
projectWithSecrets("", "", "./testdata/config2.txt"),
112+
serviceConfig("myContext2", "never", 2),
113+
)
114+
assert.NilError(t, err)
115+
assert.Assert(t, hash1 != hash2)
116+
}
117+
118+
func projectWithConfigs(configContent, configEnvironmentValue, configFile string) *types.Project {
119+
envName := "myEnv"
120+
121+
if configEnvironmentValue == "" {
122+
envName = ""
123+
}
124+
81125
return &types.Project{
126+
Environment: types.Mapping{
127+
envName: configEnvironmentValue,
128+
},
82129
Configs: types.Configs{
83-
configName: types.ConfigObjConfig{
130+
"myConfigSource": types.ConfigObjConfig{
84131
Content: configContent,
85-
Environment: configEnvironment,
132+
Environment: envName,
86133
File: configFile,
87134
},
88135
},
89136
}
90137
}
91138

139+
func projectWithSecrets(secretContent, secretEnvironmentValue, secretFile string) *types.Project {
140+
envName := "myEnv"
141+
142+
if secretEnvironmentValue == "" {
143+
envName = ""
144+
}
145+
146+
return &types.Project{
147+
Environment: types.Mapping{
148+
envName: secretEnvironmentValue,
149+
},
150+
Secrets: types.Secrets{
151+
"mySecretSource": types.SecretConfig{
152+
Content: secretContent,
153+
Environment: envName,
154+
File: secretFile,
155+
},
156+
},
157+
}
158+
}
159+
92160
func serviceConfig(buildContext, pullPolicy string, replicas int) types.ServiceConfig {
93161
return types.ServiceConfig{
94162
Build: &types.BuildConfig{
@@ -106,5 +174,10 @@ func serviceConfig(buildContext, pullPolicy string, replicas int) types.ServiceC
106174
Source: "myConfigSource",
107175
},
108176
},
177+
Secrets: []types.ServiceSecretConfig{
178+
{
179+
Source: "mySecretSource",
180+
},
181+
},
109182
}
110183
}

0 commit comments

Comments
 (0)