Skip to content

Commit a694d81

Browse files
committed
do not interpolate service.environment
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent a0d3b94 commit a694d81

File tree

8 files changed

+116
-76
lines changed

8 files changed

+116
-76
lines changed

dotenv/godotenv.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func ReadFile(filename string, lookupFn LookupFn) (map[string]string, error) {
167167
return ParseWithLookup(file, lookupFn)
168168
}
169169

170-
func expandVariables(value string, envMap map[string]string, lookupFn LookupFn) (string, error) {
170+
func ExpandVariables(value string, envMap map[string]string, lookupFn LookupFn) (string, error) {
171171
retVal, err := template.Substitute(value, func(k string) (string, bool) {
172172
if v, ok := lookupFn(k); ok {
173173
return v, true

dotenv/godotenv_var_expansion_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestExpandIfEmptyOrUnset(t *testing.T) {
4343

4444
for _, expected := range templateResults {
4545
t.Run(expected.name, func(t *testing.T) {
46-
result, err := expandVariables(expected.input, envMap, notFoundLookup)
46+
result, err := ExpandVariables(expected.input, envMap, notFoundLookup)
4747
require.NoError(t, err)
4848
assert.Equal(t, expected.result, result)
4949
})
@@ -75,7 +75,7 @@ func TestExpandIfUnset(t *testing.T) {
7575

7676
for _, expected := range templateResults {
7777
t.Run(expected.name, func(t *testing.T) {
78-
result, err := expandVariables(expected.input, envMap, notFoundLookup)
78+
result, err := ExpandVariables(expected.input, envMap, notFoundLookup)
7979
require.NoError(t, err)
8080
assert.Equal(t, expected.result, result)
8181
})
@@ -111,7 +111,7 @@ func TestErrorIfEmptyOrUnset(t *testing.T) {
111111

112112
for _, expected := range templateResults {
113113
t.Run(expected.name, func(t *testing.T) {
114-
result, err := expandVariables(expected.input, envMap, notFoundLookup)
114+
result, err := ExpandVariables(expected.input, envMap, notFoundLookup)
115115
assert.Equal(t, expected.err, err)
116116
assert.Equal(t, expected.result, result)
117117
})
@@ -147,7 +147,7 @@ func TestErrorIfUnset(t *testing.T) {
147147

148148
for _, expected := range templateResults {
149149
t.Run(expected.name, func(t *testing.T) {
150-
result, err := expandVariables(expected.input, envMap, notFoundLookup)
150+
result, err := ExpandVariables(expected.input, envMap, notFoundLookup)
151151
assert.Equal(t, expected.err, err)
152152
assert.Equal(t, expected.result, result)
153153
})

dotenv/parser.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (p *parser) extractVarValue(src string, envMap map[string]string, lookupFn
157157
// Remove inline comments on unquoted lines
158158
value, _, _ = strings.Cut(value, " #")
159159
value = strings.TrimRightFunc(value, unicode.IsSpace)
160-
retVal, err := expandVariables(value, envMap, lookupFn)
160+
retVal, err := ExpandVariables(value, envMap, lookupFn)
161161
return retVal, rest, err
162162
}
163163

@@ -194,7 +194,7 @@ func (p *parser) extractVarValue(src string, envMap map[string]string, lookupFn
194194
if quote == prefixDoubleQuote {
195195
// expand standard shell escape sequences & then interpolate
196196
// variables on the result
197-
retVal, err := expandVariables(expandEscapes(value), envMap, lookupFn)
197+
retVal, err := ExpandVariables(expandEscapes(value), envMap, lookupFn)
198198
if err != nil {
199199
return "", "", err
200200
}

interpolation/interpolation.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type Options struct {
3333
TypeCastMapping map[tree.Path]Cast
3434
// Substitution function to use
3535
Substitute func(string, template.Mapping) (string, error)
36+
// Exclude do not run interpolation on matching paths
37+
Exclude []tree.Path
3638
}
3739

3840
// LookupValue is a function which maps from variable names to values.
@@ -70,6 +72,11 @@ func Interpolate(config map[string]interface{}, opts Options) (map[string]interf
7072
}
7173

7274
func recursiveInterpolate(value interface{}, path tree.Path, opts Options) (interface{}, error) {
75+
for _, pattern := range opts.Exclude {
76+
if path.Matches(pattern) {
77+
return value, nil
78+
}
79+
}
7380
switch value := value.(type) {
7481
case string:
7582
newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue))

loader/loader.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,9 @@ func toOptions(configDetails *types.ConfigDetails, options []func(*Options)) *Op
362362
op(opts)
363363
}
364364
opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{configDetails.WorkingDir})
365+
if opts.SkipResolveEnvironment {
366+
opts.Interpolate.Exclude = append(opts.Interpolate.Exclude, "services.*.environment")
367+
}
365368
return opts
366369
}
367370

schema/compose-spec.json

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -604,52 +604,7 @@
604604
"type": "string",
605605
"description": "Target platform to run on, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'."
606606
},
607-
"ports": {
608-
"type": "array",
609-
"description": "Expose container ports. Short format ([HOST:]CONTAINER[/PROTOCOL]).",
610-
"items": {
611-
"oneOf": [
612-
{"type": "number"},
613-
{"type": "string"},
614-
{
615-
"type": "object",
616-
"properties": {
617-
"name": {
618-
"type": "string",
619-
"description": "A human-readable name for this port mapping."
620-
},
621-
"mode": {
622-
"type": "string",
623-
"description": "The port binding mode, either 'host' for publishing a host port or 'ingress' for load balancing."
624-
},
625-
"host_ip": {
626-
"type": "string",
627-
"description": "The host IP to bind to."
628-
},
629-
"target": {
630-
"type": ["integer", "string"],
631-
"description": "The port inside the container."
632-
},
633-
"published": {
634-
"type": ["string", "integer"],
635-
"description": "The publicly exposed port."
636-
},
637-
"protocol": {
638-
"type": "string",
639-
"description": "The port protocol (tcp or udp)."
640-
},
641-
"app_protocol": {
642-
"type": "string",
643-
"description": "Application protocol to use with the port (e.g., http, https, mysql)."
644-
}
645-
},
646-
"additionalProperties": false,
647-
"patternProperties": {"^x-": {}}
648-
}
649-
]
650-
},
651-
"uniqueItems": true
652-
},
607+
"ports": {"$ref": "#/definitions/ports"},
653608
"post_start": {
654609
"type": "array",
655610
"items": {"$ref": "#/definitions/service_hook"},
@@ -929,6 +884,14 @@
929884
"type": ["object", "null"],
930885
"description": "Development configuration for the service, used for development workflows.",
931886
"properties": {
887+
"command": {
888+
"$ref": "#/definitions/command",
889+
"description": "Command to execute in development mode."
890+
},
891+
"ports": {
892+
"$ref": "#/definitions/ports",
893+
"description": "Additional ports to expose in development mode. Short format ([HOST:]CONTAINER[/PROTOCOL])."
894+
},
932895
"watch": {
933896
"type": "array",
934897
"description": "Configure watch mode for the service, which monitors file changes and performs actions in response.",
@@ -1589,6 +1552,53 @@
15891552
"required": ["command"]
15901553
},
15911554

1555+
"ports": {
1556+
"type": "array",
1557+
"description": "Expose container ports. Short format ([HOST:]CONTAINER[/PROTOCOL]).",
1558+
"items": {
1559+
"oneOf": [
1560+
{"type": "number"},
1561+
{"type": "string"},
1562+
{
1563+
"type": "object",
1564+
"properties": {
1565+
"name": {
1566+
"type": "string",
1567+
"description": "A human-readable name for this port mapping."
1568+
},
1569+
"mode": {
1570+
"type": "string",
1571+
"description": "The port binding mode, either 'host' for publishing a host port or 'ingress' for load balancing."
1572+
},
1573+
"host_ip": {
1574+
"type": "string",
1575+
"description": "The host IP to bind to."
1576+
},
1577+
"target": {
1578+
"type": ["integer", "string"],
1579+
"description": "The port inside the container."
1580+
},
1581+
"published": {
1582+
"type": ["string", "integer"],
1583+
"description": "The publicly exposed port."
1584+
},
1585+
"protocol": {
1586+
"type": "string",
1587+
"description": "The port protocol (tcp or udp)."
1588+
},
1589+
"app_protocol": {
1590+
"type": "string",
1591+
"description": "Application protocol to use with the port (e.g., http, https, mysql)."
1592+
}
1593+
},
1594+
"additionalProperties": false,
1595+
"patternProperties": {"^x-": {}}
1596+
}
1597+
]
1598+
},
1599+
"uniqueItems": true
1600+
},
1601+
15921602
"env_file": {
15931603
"oneOf": [
15941604
{

types/mapping.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ func NewMappingWithEquals(values []string) MappingWithEquals {
4343
return mapping
4444
}
4545

46+
func (m MappingWithEquals) Values() []string {
47+
values := make([]string, 0, len(m))
48+
for key, val := range m {
49+
if val != nil {
50+
values = append(values, fmt.Sprintf("%s=%s", key, val))
51+
} else {
52+
values = append(values, key)
53+
}
54+
}
55+
return values
56+
}
57+
4658
// OverrideBy update MappingWithEquals with values from another MappingWithEquals
4759
func (m MappingWithEquals) OverrideBy(other MappingWithEquals) MappingWithEquals {
4860
for k, v := range other {

types/project.go

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -648,34 +648,42 @@ func (p *Project) MarshalJSON(options ...func(*marshallOptions)) ([]byte, error)
648648
func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project, error) {
649649
newProject := p.deepCopy()
650650
for i, service := range newProject.Services {
651-
service.Environment = service.Environment.Resolve(newProject.Environment.Resolve)
652-
653-
environment := service.Environment.ToMapping()
654-
for _, envFile := range service.EnvFiles {
655-
err := loadEnvFile(envFile, environment, func(k string) (string, bool) {
656-
// project.env has precedence doing interpolation
657-
if resolve, ok := p.Environment.Resolve(k); ok {
658-
return resolve, true
659-
}
660-
// then service.environment
661-
if s, ok := service.Environment[k]; ok && s != nil {
662-
return *s, true
663-
}
664-
return "", false
665-
})
666-
if err != nil {
667-
return nil, err
668-
}
651+
err := service.WithEnvironmentResolved(discardEnvFiles, newProject)
652+
if err != nil {
653+
return nil, err
669654
}
655+
newProject.Services[i] = service
656+
}
657+
return newProject, nil
658+
}
670659

671-
service.Environment = environment.ToMappingWithEquals().OverrideBy(service.Environment)
660+
func (s *ServiceConfig) WithEnvironmentResolved(discardEnvFiles bool, p *Project) error {
661+
s.Environment = s.Environment.Resolve(p.Environment.Resolve)
672662

673-
if discardEnvFiles {
674-
service.EnvFiles = nil
663+
environment := s.Environment.ToMapping()
664+
for _, envFile := range s.EnvFiles {
665+
err := loadEnvFile(envFile, environment, func(k string) (string, bool) {
666+
// project.env has precedence doing interpolation
667+
if resolve, ok := p.Environment.Resolve(k); ok {
668+
return resolve, true
669+
}
670+
// then service.environment
671+
if s, ok := s.Environment[k]; ok && s != nil {
672+
return *s, true
673+
}
674+
return "", false
675+
})
676+
if err != nil {
677+
return err
675678
}
676-
newProject.Services[i] = service
677679
}
678-
return newProject, nil
680+
681+
s.Environment = environment.ToMappingWithEquals().OverrideBy(s.Environment)
682+
683+
if discardEnvFiles {
684+
s.EnvFiles = nil
685+
}
686+
return nil
679687
}
680688

681689
// WithServicesLabelsResolved parses label_files set for services to resolve the actual label map for services

0 commit comments

Comments
 (0)