Skip to content

Commit 90d348d

Browse files
authored
feat: added metadata properties substitutions support (#24)
Signed-off-by: Eugene Yarshevich <[email protected]>
1 parent 1c1b299 commit 90d348d

File tree

5 files changed

+280
-81
lines changed

5 files changed

+280
-81
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.19
55
require (
66
github.com/compose-spec/compose-go v1.6.0
77
github.com/imdario/mergo v0.3.13
8+
github.com/mitchellh/mapstructure v1.5.0
89
github.com/score-spec/score-go v0.0.0-20221019054335-3510902b5f8b
910
github.com/spf13/cobra v1.6.0
1011
github.com/stretchr/testify v1.8.0
@@ -16,7 +17,6 @@ require (
1617
github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb // indirect
1718
github.com/docker/go-connections v0.4.0 // indirect
1819
github.com/inconshreveable/mousetrap v1.0.1 // indirect
19-
github.com/mitchellh/mapstructure v1.5.0 // indirect
2020
github.com/opencontainers/go-digest v1.0.0 // indirect
2121
github.com/pmezard/go-difflib v1.0.0 // indirect
2222
github.com/spf13/pflag v1.0.5 // indirect

internal/compose/convert.go

Lines changed: 7 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,24 @@ package compose
1010
import (
1111
"errors"
1212
"fmt"
13-
"os"
14-
"regexp"
1513
"sort"
16-
"strings"
1714

1815
compose "github.com/compose-spec/compose-go/types"
1916
score "github.com/score-spec/score-go/types"
2017
)
2118

2219
// ConvertSpec converts SCORE specification into docker-compose configuration.
2320
func ConvertSpec(spec *score.WorkloadSpec) (*compose.Project, ExternalVariables, error) {
21+
context, err := buildContext(spec.Metadata, spec.Resources)
22+
if err != nil {
23+
return nil, nil, fmt.Errorf("preparing context: %w", err)
24+
}
2425

2526
for _, cSpec := range spec.Containers {
26-
var externalVars = ExternalVariables(resourcesMap(spec.Resources).listVars())
27+
var externalVars = ExternalVariables(context.ListEnvVars())
2728
var env = make(compose.MappingWithEquals, len(cSpec.Variables))
2829
for key, val := range cSpec.Variables {
29-
var envVarVal = os.Expand(val, resourcesMap(spec.Resources).mapVar)
30+
var envVarVal = context.Substitute(val)
3031
env[key] = &envVarVal
3132
}
3233

@@ -68,7 +69,7 @@ func ConvertSpec(spec *score.WorkloadSpec) (*compose.Project, ExternalVariables,
6869
}
6970
volumes[idx] = compose.ServiceVolumeConfig{
7071
Type: "volume",
71-
Source: resourceRefRegex.ReplaceAllString(vol.Source, "$1"),
72+
Source: context.Substitute(vol.Source),
7273
Target: vol.Target,
7374
ReadOnly: vol.ReadOnly,
7475
}
@@ -104,71 +105,3 @@ func ConvertSpec(spec *score.WorkloadSpec) (*compose.Project, ExternalVariables,
104105

105106
return nil, nil, errors.New("workload does not have any containers to convert into a compose service")
106107
}
107-
108-
// resourceRefRegex extracts the resource ID from the resource reference: '${resources.RESOURCE_ID}'
109-
var resourceRefRegex = regexp.MustCompile(`\${resources\.(.+)}`)
110-
111-
// resourcesMap is an internal utility type to group some helper methods.
112-
type resourcesMap map[string]score.ResourceSpec
113-
114-
// listResourcesVars lists all available environment variables based on the declared resources properties.
115-
func (r resourcesMap) listVars() map[string]interface{} {
116-
var vars = make(map[string]interface{})
117-
for resName, res := range r {
118-
for propName, prop := range res.Properties {
119-
var envVar string
120-
switch res.Type {
121-
case "environment":
122-
envVar = strings.ToUpper(propName)
123-
default:
124-
envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
125-
}
126-
127-
envVar = strings.Replace(envVar, "-", "_", -1)
128-
envVar = strings.Replace(envVar, ".", "_", -1)
129-
130-
vars[envVar] = prop.Default
131-
}
132-
}
133-
return vars
134-
}
135-
136-
// mapResourceVar maps resources properties references.
137-
// Returns an empty string if the reference can't be resolved.
138-
func (r resourcesMap) mapVar(ref string) string {
139-
if ref == "$" {
140-
return ref
141-
}
142-
143-
var segments = strings.SplitN(ref, ".", 3)
144-
if segments[0] != "resources" || len(segments) != 3 {
145-
return ""
146-
}
147-
148-
var resName = segments[1]
149-
var propName = segments[2]
150-
if res, ok := r[resName]; ok {
151-
if prop, ok := res.Properties[propName]; ok {
152-
var envVar string
153-
switch res.Type {
154-
case "environment":
155-
envVar = strings.ToUpper(propName)
156-
default:
157-
envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
158-
}
159-
160-
envVar = strings.Replace(envVar, "-", "_", -1)
161-
envVar = strings.Replace(envVar, ".", "_", -1)
162-
163-
if prop.Default != nil {
164-
envVar += fmt.Sprintf("-%v", prop.Default)
165-
} else if prop.Required {
166-
envVar += "?err"
167-
}
168-
169-
return fmt.Sprintf("${%s}", envVar)
170-
}
171-
}
172-
173-
return ""
174-
}

internal/compose/convert_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,14 @@ func TestScoreConvert(t *testing.T) {
175175
},
176176
},
177177
Vars: ExternalVariables{
178-
"DEBUG": false,
178+
"DEBUG": "false",
179179
"LOGS_LEVEL": "WARN",
180180
"APP_DB_HOST": "localhost",
181-
"APP_DB_PORT": 5432,
182-
"APP_DB_NAME": nil,
183-
"APP_DB_USER_NAME": nil,
184-
"APP_DB_PASSWORD": nil,
185-
"DNS_DOMAIN": nil,
181+
"APP_DB_PORT": "5432",
182+
"APP_DB_NAME": "",
183+
"APP_DB_USER_NAME": "",
184+
"APP_DB_PASSWORD": "",
185+
"DNS_DOMAIN": "",
186186
},
187187
},
188188

internal/compose/templates.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
Apache Score
3+
Copyright 2022 The Apache Software Foundation
4+
5+
This product includes software developed at
6+
The Apache Software Foundation (http://www.apache.org/).
7+
*/
8+
package compose
9+
10+
import (
11+
"fmt"
12+
"log"
13+
"os"
14+
"regexp"
15+
"strings"
16+
17+
"github.com/mitchellh/mapstructure"
18+
19+
score "github.com/score-spec/score-go/types"
20+
)
21+
22+
// templatesContext ia an utility type that provides a context for '${...}' templates substitution
23+
type templatesContext map[string]string
24+
25+
// buildContext initializes a new templatesContext instance
26+
func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs) (templatesContext, error) {
27+
var ctx = make(map[string]string)
28+
29+
var metadataMap = make(map[string]interface{})
30+
if decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
31+
TagName: "json",
32+
Result: &metadataMap,
33+
}); err != nil {
34+
return nil, err
35+
} else {
36+
decoder.Decode(metadata)
37+
for key, val := range metadataMap {
38+
var ref = fmt.Sprintf("metadata.%s", key)
39+
if _, exists := ctx[ref]; exists {
40+
return nil, fmt.Errorf("ambiguous property reference '%s'", ref)
41+
}
42+
ctx[ref] = fmt.Sprintf("%v", val)
43+
}
44+
}
45+
46+
for resName, res := range resources {
47+
ctx[fmt.Sprintf("resources.%s", resName)] = resName
48+
49+
for propName, prop := range res.Properties {
50+
var ref = fmt.Sprintf("resources.%s.%s", resName, propName)
51+
52+
var envVar string
53+
switch res.Type {
54+
case "environment":
55+
envVar = strings.ToUpper(propName)
56+
default:
57+
envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
58+
}
59+
60+
envVar = strings.Replace(envVar, "-", "_", -1)
61+
envVar = strings.Replace(envVar, ".", "_", -1)
62+
63+
if prop.Default != nil {
64+
envVar += fmt.Sprintf("-%v", prop.Default)
65+
} else if prop.Required {
66+
envVar += "?err"
67+
}
68+
69+
ctx[ref] = fmt.Sprintf("${%s}", envVar)
70+
}
71+
}
72+
73+
return ctx, nil
74+
}
75+
76+
// Substitute replaces all matching '${...}' templates in a source string
77+
func (context templatesContext) Substitute(src string) string {
78+
return os.Expand(src, context.mapVar)
79+
}
80+
81+
// MapVar replaces objects and properties references with corresponding values
82+
// Returns an empty string if the reference can't be resolved
83+
func (context templatesContext) mapVar(ref string) string {
84+
if ref == "" {
85+
return ""
86+
}
87+
88+
// NOTE: os.Expand(..) would invoke a callback function with "$" as an argument for escaped sequences.
89+
// "$${abc}" is treated as "$$" pattern and "{abc}" static text.
90+
// The first segment (pattern) would trigger a callback function call.
91+
// By returning "$" value we would ensure that escaped sequences would remain in the source text.
92+
// For example "$${abc}" would result in "${abc}" after os.Expand(..) call.
93+
if ref == "$" {
94+
return ref
95+
}
96+
97+
if res, ok := context[ref]; ok {
98+
return res
99+
}
100+
101+
log.Printf("Warning: Can not resolve '%s'. Resource or property is not declared.", ref)
102+
return ""
103+
}
104+
105+
// composeEnvVarReferencePattern defines the rule for compose environment variable references
106+
// Possible documented references for compose v3.5:
107+
// - ${ENV_VAR}
108+
// - ${ENV_VAR?err}
109+
// - ${ENV_VAR-default}
110+
var envVarPattern = regexp.MustCompile(`\$\{(\w+)(?:\-(.+?)|\?.+)?\}$`)
111+
112+
// ListEnvVars reports all environment variables used by templatesContext
113+
func (context templatesContext) ListEnvVars() map[string]interface{} {
114+
var vars = make(map[string]interface{})
115+
for _, ref := range context {
116+
if matches := envVarPattern.FindStringSubmatch(ref); len(matches) == 3 {
117+
vars[matches[1]] = matches[2]
118+
}
119+
}
120+
return vars
121+
}

0 commit comments

Comments
 (0)