diff --git a/schema/compose-spec.json b/schema/compose-spec.json index 0469c9e0d..426bd4905 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -215,7 +215,7 @@ "dns_search": {"$ref": "#/definitions/string_or_list"}, "domainname": {"type": "string"}, "entrypoint": {"$ref": "#/definitions/command"}, - "env_file": {"$ref": "#/definitions/string_or_list"}, + "env_file": {"$ref": "#/definitions/env_file"}, "environment": {"$ref": "#/definitions/list_or_dict"}, "expose": { @@ -775,6 +775,25 @@ ] }, + "env_file": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": {"type": "string"}, + "required": {"type": "boolean", "default": true} + }, + "required": ["path"] + } + } + ] + }, + "string_or_list": { "oneOf": [ {"type": "string"}, diff --git a/transform/build.go b/transform/build.go index 505228c36..526dedbd2 100644 --- a/transform/build.go +++ b/transform/build.go @@ -33,6 +33,6 @@ func transformBuild(data any, p tree.Path) (any, error) { "context": v, }, nil default: - return data, errors.Errorf("invalid type %T for build", v) + return data, errors.Errorf("%s: invalid type %T for build", p, v) } } diff --git a/transform/canonical.go b/transform/canonical.go index 6569f437f..1c9f986d9 100644 --- a/transform/canonical.go +++ b/transform/canonical.go @@ -28,6 +28,7 @@ func init() { transformers["services.*"] = transformService transformers["services.*.build.secrets.*"] = transformFileMount transformers["services.*.depends_on"] = transformDependsOn + transformers["services.*.env_file"] = transformEnvFile transformers["services.*.extends"] = transformExtends transformers["services.*.networks"] = transformServiceNetworks transformers["services.*.volumes.*"] = transformVolumeMount diff --git a/transform/dependson.go b/transform/dependson.go index fbaaff262..29772a5f3 100644 --- a/transform/dependson.go +++ b/transform/dependson.go @@ -49,6 +49,6 @@ func transformDependsOn(data any, p tree.Path) (any, error) { } return d, nil default: - return data, errors.Errorf("invalid type %T for depend_on", v) + return data, errors.Errorf("%s: invalid type %T for depend_on", p, v) } } diff --git a/transform/envfile.go b/transform/envfile.go new file mode 100644 index 000000000..9715ad826 --- /dev/null +++ b/transform/envfile.go @@ -0,0 +1,54 @@ +/* + Copyright 2020 The Compose Specification Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package transform + +import ( + "github.com/compose-spec/compose-go/v2/tree" + "github.com/pkg/errors" +) + +func transformEnvFile(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case string: + return []any{ + transformEnvFileValue(v), + }, nil + case []any: + for i, e := range v { + v[i] = transformEnvFileValue(e) + } + return v, nil + default: + return nil, errors.Errorf("%s: invalid type %T for env_file", p, v) + } +} + +func transformEnvFileValue(data any) any { + switch v := data.(type) { + case string: + return map[string]any{ + "path": v, + "required": true, + } + case map[string]any: + if _, ok := v["required"]; !ok { + v["required"] = true + } + return v + } + return nil +} diff --git a/transform/envfile_test.go b/transform/envfile_test.go new file mode 100644 index 000000000..dcad6a5ec --- /dev/null +++ b/transform/envfile_test.go @@ -0,0 +1,79 @@ +/* + Copyright 2020 The Compose Specification Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package transform + +import ( + "testing" + + "github.com/compose-spec/compose-go/v2/tree" + "gopkg.in/yaml.v3" + "gotest.tools/v3/assert" +) + +func TestSingle(t *testing.T) { + env, err := transformEnvFile(".env", tree.NewPath("service.test.env_file")) + assert.NilError(t, err) + assert.DeepEqual(t, env, []map[string]any{ + { + "path": ".env", + "required": true, + }, + }) +} + +func TestSequence(t *testing.T) { + var in any + err := yaml.Unmarshal([]byte(` + - .env + - other.env +`), &in) + assert.NilError(t, err) + env, err := transformEnvFile(in, tree.NewPath("service.test.env_file")) + assert.NilError(t, err) + assert.DeepEqual(t, env, []any{ + map[string]any{ + "path": ".env", + "required": true, + }, + map[string]any{ + "path": "other.env", + "required": true, + }, + }) +} + +func TestOptional(t *testing.T) { + var in any + err := yaml.Unmarshal([]byte(` + - .env + - path: other.env + required: false +`), &in) + assert.NilError(t, err) + env, err := transformEnvFile(in, tree.NewPath("service.test.env_file")) + assert.NilError(t, err) + assert.DeepEqual(t, env, []any{ + map[string]any{ + "path": ".env", + "required": true, + }, + map[string]any{ + "path": "other.env", + "required": false, + }, + }) +} diff --git a/transform/extends.go b/transform/extends.go index 6c65f7bbb..feb965d13 100644 --- a/transform/extends.go +++ b/transform/extends.go @@ -30,6 +30,6 @@ func transformExtends(data any, p tree.Path) (any, error) { "service": v, }, nil default: - return data, errors.Errorf("invalid type %T for extends", v) + return data, errors.Errorf("%s: invalid type %T for extends", p, v) } } diff --git a/transform/include.go b/transform/include.go index c28a94ff1..b967c1857 100644 --- a/transform/include.go +++ b/transform/include.go @@ -21,7 +21,7 @@ import ( "github.com/pkg/errors" ) -func transformInclude(data any, _ tree.Path) (any, error) { +func transformInclude(data any, p tree.Path) (any, error) { switch v := data.(type) { case map[string]any: return v, nil @@ -30,6 +30,6 @@ func transformInclude(data any, _ tree.Path) (any, error) { "path": v, }, nil default: - return data, errors.Errorf("invalid type %T for external", v) + return data, errors.Errorf("%s: invalid type %T for external", p, v) } } diff --git a/transform/ports.go b/transform/ports.go index b61807383..231a75a5a 100644 --- a/transform/ports.go +++ b/transform/ports.go @@ -25,7 +25,7 @@ import ( "github.com/pkg/errors" ) -func transformPorts(data any, _ tree.Path) (any, error) { +func transformPorts(data any, p tree.Path) (any, error) { switch entries := data.(type) { case []any: // We process the list instead of individual items here. @@ -64,12 +64,12 @@ func transformPorts(data any, _ tree.Path) (any, error) { case map[string]any: ports = append(ports, value) default: - return data, errors.Errorf("invalid type %T for port", value) + return data, errors.Errorf("%s: invalid type %T for port", p, value) } } return ports, nil default: - return data, errors.Errorf("invalid type %T for port", entries) + return data, errors.Errorf("%s: invalid type %T for port", p, entries) } } diff --git a/transform/ssh.go b/transform/ssh.go index 640a10881..ca71e13c4 100644 --- a/transform/ssh.go +++ b/transform/ssh.go @@ -24,7 +24,7 @@ import ( "github.com/pkg/errors" ) -func transformSSH(data any, _ tree.Path) (any, error) { +func transformSSH(data any, p tree.Path) (any, error) { switch v := data.(type) { case map[string]any: return v, nil @@ -47,6 +47,6 @@ func transformSSH(data any, _ tree.Path) (any, error) { } return result, nil default: - return data, errors.Errorf("invalid type %T for ssh", v) + return data, errors.Errorf("%s: invalid type %T for ssh", p, v) } } diff --git a/transform/ulimits.go b/transform/ulimits.go index ba9e38790..80a997af7 100644 --- a/transform/ulimits.go +++ b/transform/ulimits.go @@ -21,7 +21,7 @@ import ( "github.com/pkg/errors" ) -func transformUlimits(data any, _ tree.Path) (any, error) { +func transformUlimits(data any, p tree.Path) (any, error) { switch v := data.(type) { case map[string]any: return v, nil @@ -30,6 +30,6 @@ func transformUlimits(data any, _ tree.Path) (any, error) { "single": v, }, nil default: - return data, errors.Errorf("invalid type %T for external", v) + return data, errors.Errorf("%s: invalid type %T for external", p, v) } } diff --git a/transform/volume.go b/transform/volume.go index c1d592b2d..757177fcd 100644 --- a/transform/volume.go +++ b/transform/volume.go @@ -22,7 +22,7 @@ import ( "github.com/pkg/errors" ) -func transformVolumeMount(data any, _ tree.Path) (any, error) { +func transformVolumeMount(data any, p tree.Path) (any, error) { switch v := data.(type) { case map[string]any: return v, nil @@ -34,6 +34,6 @@ func transformVolumeMount(data any, _ tree.Path) (any, error) { return encode(volume) default: - return data, errors.Errorf("invalid type %T for service volume mount", v) + return data, errors.Errorf("%s: invalid type %T for service volume mount", p, v) } } diff --git a/types/envfile.go b/types/envfile.go new file mode 100644 index 000000000..72de879d7 --- /dev/null +++ b/types/envfile.go @@ -0,0 +1,41 @@ +/* + Copyright 2020 The Compose Specification Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package types + +import "encoding/json" + +type EnvFile struct { + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Required bool `yaml:"required" json:"required"` +} + +// MarshalYAML makes EnvFile implement yaml.Marshaler +func (e *EnvFile) MarshalYAML() (interface{}, error) { + if e.Required { + return e.Path, nil + } + return e, nil +} + +// MarshalYAML makes EnvFile implement json.Marshaler +func (e *EnvFile) MarshalJSON() ([]byte, error) { + if e.Required { + return []byte(e.Path), nil + } + // Pass as a value to avoid re-entering this method and use the default implementation + return json.Marshal(*e) +} diff --git a/types/project.go b/types/project.go index a3c4e7f7a..f16d8fe0b 100644 --- a/types/project.go +++ b/types/project.go @@ -533,10 +533,16 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error { return p.Environment.Resolve(s) } - for _, envFile := range service.EnvFile { - b, err := os.ReadFile(envFile) + for _, envFile := range service.EnvFiles { + if _, err := os.Stat(envFile.Path); os.IsNotExist(err) { + if envFile.Required { + return errors.Wrapf(err, "env file %s not found", envFile) + } + continue + } + b, err := os.ReadFile(envFile.Path) if err != nil { - return errors.Wrapf(err, "Failed to load %s", envFile) + return errors.Wrapf(err, "failed to load %s", envFile) } fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve) @@ -549,7 +555,7 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error { service.Environment = environment.OverrideBy(service.Environment) if discardEnvFiles { - service.EnvFile = nil + service.EnvFiles = nil } p.Services[i] = service } diff --git a/types/types.go b/types/types.go index 17229af19..2eb756807 100644 --- a/types/types.go +++ b/types/types.go @@ -75,7 +75,7 @@ type ServiceConfig struct { Entrypoint ShellCommand `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details. Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"` - EnvFile StringList `yaml:"env_file,omitempty" json:"env_file,omitempty"` + EnvFiles []EnvFile `yaml:"env_file,omitempty" json:"env_file,omitempty"` Expose StringOrNumberList `yaml:"expose,omitempty" json:"expose,omitempty"` Extends *ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"` ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"`