Repository: compose-spec/compose-go
Pull request #263 (2022-05-18 07:48:01)

add support for secret set by environment variable
implements https://github.com/compose-spec/compose-spec/pull/257


Patch:
diff --git a/loader/full-example.yml b/loader/full-example.yml
index 8cd8331..1969527 100644
--- a/loader/full-example.yml
+++ b/loader/full-example.yml
@@ -405,6 +405,7 @@ configs:
     external: true
   config4:
     name: foo
+    file: ~/config_data
     x-bar: baz
     x-foo: bar
 
@@ -420,6 +421,7 @@ secrets:
     external: true
   secret4:
     name: bar
+    environment: BAR
     x-bar: baz
     x-foo: bar
 x-bar: baz
diff --git a/loader/loader.go b/loader/loader.go
index b008099..01c0c5d 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -804,7 +804,7 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi
 			return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
 		}
 	default:
-		if resolvePaths {
+		if obj.File != "" && resolvePaths {
 			obj.File = absPath(details.WorkingDir, obj.File)
 		}
 	}
diff --git a/loader/validate.go b/loader/validate.go
index 727e0b2..d8ab619 100644
--- a/loader/validate.go
+++ b/loader/validate.go
@@ -68,5 +68,15 @@ func checkConsistency(project *types.Project) error {
 			}
 		}
 	}
+
+	for name, secret := range project.Secrets {
+		if secret.External.External {
+			continue
+		}
+		if secret.File == "" && secret.Environment == "" {
+			return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("secret %q must declare either `file` or `environment`", name))
+		}
+	}
+
 	return nil
 }
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index a41e056..2500ce7 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -685,6 +685,7 @@
       "type": "object",
       "properties": {
         "name": {"type": "string"},
+        "environment": {"type": "string"},
         "file": {"type": "string"},
         "external": {
           "type": ["boolean", "object"],
diff --git a/types/types.go b/types/types.go
index 7051bf0..b54679e 100644
--- a/types/types.go
+++ b/types/types.go
@@ -891,6 +891,7 @@ type CredentialSpecConfig struct {
 type FileObjectConfig struct {
 	Name           string                 `yaml:",omitempty" json:"name,omitempty"`
 	File           string                 `yaml:",omitempty" json:"file,omitempty"`
+	Environment    string                 `yaml:",omitempty" json:"environment,omitempty"`
 	External       External               `yaml:",omitempty" json:"external,omitempty"`
 	Labels         Labels                 `yaml:",omitempty" json:"labels,omitempty"`
 	Driver         string                 `yaml:",omitempty" json:"driver,omitempty"`


Test patch:
diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go
index 6438b36..d661657 100644
--- a/loader/full-struct_test.go
+++ b/loader/full-struct_test.go
@@ -31,7 +31,7 @@ func fullExampleConfig(workingDir, homeDir string) *types.Config {
 		Services: services(workingDir, homeDir),
 		Networks: networks(),
 		Volumes:  volumes(),
-		Configs:  configs(workingDir),
+		Configs:  configs(workingDir, homeDir),
 		Secrets:  secrets(workingDir),
 		Extensions: map[string]interface{}{
 			"x-foo": "bar",
@@ -520,7 +520,7 @@ func volumes() map[string]types.VolumeConfig {
 	}
 }
 
-func configs(workingDir string) map[string]types.ConfigObjConfig {
+func configs(workingDir string, homeDir string) map[string]types.ConfigObjConfig {
 	return map[string]types.ConfigObjConfig{
 		"config1": {
 			File: filepath.Join(workingDir, "config_data"),
@@ -538,7 +538,7 @@ func configs(workingDir string) map[string]types.ConfigObjConfig {
 		},
 		"config4": {
 			Name: "foo",
-			File: workingDir,
+			File: filepath.Join(homeDir, "config_data"),
 			Extensions: map[string]interface{}{
 				"x-bar": "baz",
 				"x-foo": "bar",
@@ -564,8 +564,8 @@ func secrets(workingDir string) map[string]types.SecretConfig {
 			External: types.External{External: true},
 		},
 		"secret4": {
-			Name: "bar",
-			File: workingDir,
+			Name:        "bar",
+			Environment: "BAR",
 			Extensions: map[string]interface{}{
 				"x-bar": "baz",
 				"x-foo": "bar",
@@ -973,7 +973,7 @@ secrets:
     external: true
   secret4:
     name: bar
-    file: %s
+    environment: BAR
     x-bar: baz
     x-foo: bar
 configs:
@@ -1003,9 +1003,8 @@ x-nested:
 		filepath.Join(homeDir, "configs"),
 		filepath.Join(workingDir, "opt"),
 		filepath.Join(workingDir, "secret_data"),
-		filepath.Join(workingDir),
 		filepath.Join(workingDir, "config_data"),
-		filepath.Join(workingDir))
+		filepath.Join(homeDir, "config_data"))
 }
 
 func fullExampleJSON(workingDir, homeDir string) string {
@@ -1096,7 +1095,7 @@ func fullExampleJSON(workingDir, homeDir string) string {
     },
     "secret4": {
       "name": "bar",
-      "file": "%s",
+      "environment": "BAR",
       "external": false
     }
   },
@@ -1613,10 +1612,9 @@ func fullExampleJSON(workingDir, homeDir string) string {
   }
 }`,
 		toPath(workingDir, "config_data"),
-		toPath(workingDir),
+		toPath(homeDir, "config_data"),
 		toPath(workingDir, "secret_data"),
 		toPath(workingDir),
-		toPath(workingDir),
 		toPath(workingDir, "static"),
 		toPath(homeDir, "configs"),
 		toPath(workingDir, "opt"))
diff --git a/loader/loader_test.go b/loader/loader_test.go
index c3baec5..8a9cc1d 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -917,7 +917,11 @@ func TestFullExample(t *testing.T) {
 
 	homeDir, err := os.UserHomeDir()
 	assert.NilError(t, err)
-	env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
+	env := map[string]string{
+		"HOME": homeDir,
+		"BAR":  "this is a secret",
+		"QUX":  "qux_from_environment",
+	}
 	config, err := loadYAMLWithEnv(string(b), env)
 	assert.NilError(t, err)
 
diff --git a/loader/validate_test.go b/loader/validate_test.go
index 15a6f34..6d423d2 100644
--- a/loader/validate_test.go
+++ b/loader/validate_test.go
@@ -139,3 +139,48 @@ func TestValidateNetworkMode(t *testing.T) {
 		assert.NilError(t, err)
 	})
 }
+
+func TestValidateSecret(t *testing.T) {
+	t.Run("secret set by file", func(t *testing.T) {
+		project := &types.Project{
+			Secrets: types.Secrets{
+				"foo": types.SecretConfig{
+					File: ".secret",
+				},
+			},
+		}
+		err := checkConsistency(project)
+		assert.NilError(t, err)
+	})
+	t.Run("secret set by environment", func(t *testing.T) {
+		project := &types.Project{
+			Secrets: types.Secrets{
+				"foo": types.SecretConfig{
+					Environment: "TOKEN",
+				},
+			},
+		}
+		err := checkConsistency(project)
+		assert.NilError(t, err)
+	})
+	t.Run("external secret", func(t *testing.T) {
+		project := &types.Project{
+			Secrets: types.Secrets{
+				"foo": types.SecretConfig{
+					External: types.External{External: true},
+				},
+			},
+		}
+		err := checkConsistency(project)
+		assert.NilError(t, err)
+	})
+	t.Run("uset secret", func(t *testing.T) {
+		project := &types.Project{
+			Secrets: types.Secrets{
+				"foo": types.SecretConfig{},
+			},
+		}
+		err := checkConsistency(project)
+		assert.Error(t, err, "secret \"foo\" must declare either `file` or `environment`: invalid compose project")
+	})
+}