Skip to content

Commit

Permalink
feat: added metadata properties substitutions support (#24)
Browse files Browse the repository at this point in the history
Signed-off-by: Eugene Yarshevich <[email protected]>
  • Loading branch information
ghen authored Dec 22, 2022
1 parent 1c1b299 commit 90d348d
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 81 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.19
require (
github.com/compose-spec/compose-go v1.6.0
github.com/imdario/mergo v0.3.13
github.com/mitchellh/mapstructure v1.5.0
github.com/score-spec/score-go v0.0.0-20221019054335-3510902b5f8b
github.com/spf13/cobra v1.6.0
github.com/stretchr/testify v1.8.0
Expand All @@ -16,7 +17,6 @@ require (
github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
Expand Down
81 changes: 7 additions & 74 deletions internal/compose/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,24 @@ package compose
import (
"errors"
"fmt"
"os"
"regexp"
"sort"
"strings"

compose "github.com/compose-spec/compose-go/types"
score "github.com/score-spec/score-go/types"
)

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

for _, cSpec := range spec.Containers {
var externalVars = ExternalVariables(resourcesMap(spec.Resources).listVars())
var externalVars = ExternalVariables(context.ListEnvVars())
var env = make(compose.MappingWithEquals, len(cSpec.Variables))
for key, val := range cSpec.Variables {
var envVarVal = os.Expand(val, resourcesMap(spec.Resources).mapVar)
var envVarVal = context.Substitute(val)
env[key] = &envVarVal
}

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

return nil, nil, errors.New("workload does not have any containers to convert into a compose service")
}

// resourceRefRegex extracts the resource ID from the resource reference: '${resources.RESOURCE_ID}'
var resourceRefRegex = regexp.MustCompile(`\${resources\.(.+)}`)

// resourcesMap is an internal utility type to group some helper methods.
type resourcesMap map[string]score.ResourceSpec

// listResourcesVars lists all available environment variables based on the declared resources properties.
func (r resourcesMap) listVars() map[string]interface{} {
var vars = make(map[string]interface{})
for resName, res := range r {
for propName, prop := range res.Properties {
var envVar string
switch res.Type {
case "environment":
envVar = strings.ToUpper(propName)
default:
envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
}

envVar = strings.Replace(envVar, "-", "_", -1)
envVar = strings.Replace(envVar, ".", "_", -1)

vars[envVar] = prop.Default
}
}
return vars
}

// mapResourceVar maps resources properties references.
// Returns an empty string if the reference can't be resolved.
func (r resourcesMap) mapVar(ref string) string {
if ref == "$" {
return ref
}

var segments = strings.SplitN(ref, ".", 3)
if segments[0] != "resources" || len(segments) != 3 {
return ""
}

var resName = segments[1]
var propName = segments[2]
if res, ok := r[resName]; ok {
if prop, ok := res.Properties[propName]; ok {
var envVar string
switch res.Type {
case "environment":
envVar = strings.ToUpper(propName)
default:
envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
}

envVar = strings.Replace(envVar, "-", "_", -1)
envVar = strings.Replace(envVar, ".", "_", -1)

if prop.Default != nil {
envVar += fmt.Sprintf("-%v", prop.Default)
} else if prop.Required {
envVar += "?err"
}

return fmt.Sprintf("${%s}", envVar)
}
}

return ""
}
12 changes: 6 additions & 6 deletions internal/compose/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,14 @@ func TestScoreConvert(t *testing.T) {
},
},
Vars: ExternalVariables{
"DEBUG": false,
"DEBUG": "false",
"LOGS_LEVEL": "WARN",
"APP_DB_HOST": "localhost",
"APP_DB_PORT": 5432,
"APP_DB_NAME": nil,
"APP_DB_USER_NAME": nil,
"APP_DB_PASSWORD": nil,
"DNS_DOMAIN": nil,
"APP_DB_PORT": "5432",
"APP_DB_NAME": "",
"APP_DB_USER_NAME": "",
"APP_DB_PASSWORD": "",
"DNS_DOMAIN": "",
},
},

Expand Down
121 changes: 121 additions & 0 deletions internal/compose/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
Apache Score
Copyright 2022 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
*/
package compose

import (
"fmt"
"log"
"os"
"regexp"
"strings"

"github.com/mitchellh/mapstructure"

score "github.com/score-spec/score-go/types"
)

// templatesContext ia an utility type that provides a context for '${...}' templates substitution
type templatesContext map[string]string

// buildContext initializes a new templatesContext instance
func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs) (templatesContext, error) {
var ctx = make(map[string]string)

var metadataMap = make(map[string]interface{})
if decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
Result: &metadataMap,
}); err != nil {
return nil, err
} else {
decoder.Decode(metadata)
for key, val := range metadataMap {
var ref = fmt.Sprintf("metadata.%s", key)
if _, exists := ctx[ref]; exists {
return nil, fmt.Errorf("ambiguous property reference '%s'", ref)
}
ctx[ref] = fmt.Sprintf("%v", val)
}
}

for resName, res := range resources {
ctx[fmt.Sprintf("resources.%s", resName)] = resName

for propName, prop := range res.Properties {
var ref = fmt.Sprintf("resources.%s.%s", resName, propName)

var envVar string
switch res.Type {
case "environment":
envVar = strings.ToUpper(propName)
default:
envVar = strings.ToUpper(fmt.Sprintf("%s_%s", resName, propName))
}

envVar = strings.Replace(envVar, "-", "_", -1)
envVar = strings.Replace(envVar, ".", "_", -1)

if prop.Default != nil {
envVar += fmt.Sprintf("-%v", prop.Default)
} else if prop.Required {
envVar += "?err"
}

ctx[ref] = fmt.Sprintf("${%s}", envVar)
}
}

return ctx, nil
}

// Substitute replaces all matching '${...}' templates in a source string
func (context templatesContext) Substitute(src string) string {
return os.Expand(src, context.mapVar)
}

// MapVar replaces objects and properties references with corresponding values
// Returns an empty string if the reference can't be resolved
func (context templatesContext) mapVar(ref string) string {
if ref == "" {
return ""
}

// NOTE: os.Expand(..) would invoke a callback function with "$" as an argument for escaped sequences.
// "$${abc}" is treated as "$$" pattern and "{abc}" static text.
// The first segment (pattern) would trigger a callback function call.
// By returning "$" value we would ensure that escaped sequences would remain in the source text.
// For example "$${abc}" would result in "${abc}" after os.Expand(..) call.
if ref == "$" {
return ref
}

if res, ok := context[ref]; ok {
return res
}

log.Printf("Warning: Can not resolve '%s'. Resource or property is not declared.", ref)
return ""
}

// composeEnvVarReferencePattern defines the rule for compose environment variable references
// Possible documented references for compose v3.5:
// - ${ENV_VAR}
// - ${ENV_VAR?err}
// - ${ENV_VAR-default}
var envVarPattern = regexp.MustCompile(`\$\{(\w+)(?:\-(.+?)|\?.+)?\}$`)

// ListEnvVars reports all environment variables used by templatesContext
func (context templatesContext) ListEnvVars() map[string]interface{} {
var vars = make(map[string]interface{})
for _, ref := range context {
if matches := envVarPattern.FindStringSubmatch(ref); len(matches) == 3 {
vars[matches[1]] = matches[2]
}
}
return vars
}
Loading

0 comments on commit 90d348d

Please sign in to comment.