Repository: compose-spec/compose-go
Pull request #316 (2022-11-01 21:51:05)

Check service secrets existence
[Regarding the Compose Specification](https://github.com/compose-spec/compose-spec/blob/master/spec.md#secrets), a secret used at the service level  should exist on the platform or be defined in the secrets section
>Compose implementations MUST report an error if the secret doesn't exist on the platform or isn't defined in the [secrets](https://github.com/compose-spec/compose-spec/blob/master/spec.md#secrets-top-level-element) section of this Compose file.

Also fix bugs in `WithoutUnnecessaryResources` that created empty resources when a resource declared in service section weren’t existing in the top level declaration (secrets. volumes, networks, configs...)

reference: https://github.com/docker/compose/issues/9963


Patch:
diff --git a/loader/validate.go b/loader/validate.go
index 4d63588..2b285e9 100644
--- a/loader/validate.go
+++ b/loader/validate.go
@@ -70,6 +70,12 @@ func checkConsistency(project *types.Project) error {
 				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined config %s", s.Name, config.Source))
 			}
 		}
+
+		for _, secret := range s.Secrets {
+			if _, ok := project.Secrets[secret.Source]; !ok {
+				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined secret %s", s.Name, secret.Source))
+			}
+		}
 	}
 
 	for name, secret := range project.Secrets {
diff --git a/types/project.go b/types/project.go
index 6b28e86..daa70f8 100644
--- a/types/project.go
+++ b/types/project.go
@@ -258,25 +258,33 @@ func (p *Project) WithoutUnnecessaryResources() {
 
 	networks := Networks{}
 	for k := range requiredNetworks {
-		networks[k] = p.Networks[k]
+		if value, ok := p.Networks[k]; ok {
+			networks[k] = value
+		}
 	}
 	p.Networks = networks
 
 	volumes := Volumes{}
 	for k := range requiredVolumes {
-		volumes[k] = p.Volumes[k]
+		if value, ok := p.Volumes[k]; ok {
+			volumes[k] = value
+		}
 	}
 	p.Volumes = volumes
 
 	secrets := Secrets{}
 	for k := range requiredSecrets {
-		secrets[k] = p.Secrets[k]
+		if value, ok := p.Secrets[k]; ok {
+			secrets[k] = value
+		}
 	}
 	p.Secrets = secrets
 
 	configs := Configs{}
 	for k := range requiredConfigs {
-		configs[k] = p.Configs[k]
+		if value, ok := p.Configs[k]; ok {
+			configs[k] = value
+		}
 	}
 	p.Configs = configs
 }


Test patch:
diff --git a/loader/validate_test.go b/loader/validate_test.go
index 58268b2..577d407 100644
--- a/loader/validate_test.go
+++ b/loader/validate_test.go
@@ -174,7 +174,7 @@ func TestValidateSecret(t *testing.T) {
 		err := checkConsistency(project)
 		assert.NilError(t, err)
 	})
-	t.Run("uset secret", func(t *testing.T) {
+	t.Run("unset secret type", func(t *testing.T) {
 		project := &types.Project{
 			Secrets: types.Secrets{
 				"foo": types.SecretConfig{},
@@ -183,6 +183,49 @@ func TestValidateSecret(t *testing.T) {
 		err := checkConsistency(project)
 		assert.Error(t, err, "secret \"foo\" must declare either `file` or `environment`: invalid compose project")
 	})
+
+	t.Run("service secret exist", func(t *testing.T) {
+		project := &types.Project{
+			Secrets: types.Secrets{
+				"foo": types.SecretConfig{
+					External: types.External{
+						External: true,
+					},
+				},
+			},
+			Services: types.Services([]types.ServiceConfig{
+				{
+					Name:  "myservice",
+					Image: "scratch",
+					Secrets: []types.ServiceSecretConfig{
+						{
+							Source: "foo",
+						},
+					},
+				},
+			}),
+		}
+		err := checkConsistency(project)
+		assert.NilError(t, err)
+	})
+
+	t.Run("service secret undefined", func(t *testing.T) {
+		project := &types.Project{
+			Services: types.Services([]types.ServiceConfig{
+				{
+					Name:  "myservice",
+					Image: "scratch",
+					Secrets: []types.ServiceSecretConfig{
+						{
+							Source: "foo",
+						},
+					},
+				},
+			}),
+		}
+		err := checkConsistency(project)
+		assert.Error(t, err, `service "myservice" refers to undefined secret foo: invalid compose project`)
+	})
 }
 
 func TestValidateDependsOn(t *testing.T) {