diff --git a/examples/02-environment/README.md b/examples/02-environment/README.md index c1c0366..11b4985 100644 --- a/examples/02-environment/README.md +++ b/examples/02-environment/README.md @@ -19,10 +19,6 @@ containers: resources: env: type: environment - properties: - NAME: - type: string - default: World ``` To prepare a new Humanitec deployment delta from this `score.yaml` file, use `score-humanitec` CLI tool: diff --git a/examples/02-environment/score.yaml b/examples/02-environment/score.yaml index 724e4bc..11a60e9 100644 --- a/examples/02-environment/score.yaml +++ b/examples/02-environment/score.yaml @@ -11,7 +11,3 @@ containers: resources: env: type: environment - properties: - NAME: - type: string - default: World diff --git a/examples/03-dependencies/README.md b/examples/03-dependencies/README.md index f5d11dc..98782b9 100644 --- a/examples/03-dependencies/README.md +++ b/examples/03-dependencies/README.md @@ -22,26 +22,10 @@ containers: resources: db: type: postgres - properties: - host: - default: localhost - port: - default: 5432 - name: - default: postgres - user: - secret: true - password: - secret: true dns: type: dns - properties: - domain: backend: type: service - properties: - name: - port: ``` This example also uses an extensions file, called `humanitec.yaml`, that contains additional hints for `score-humanitec` CLI tool. This information would help the CLI tool to resolve the resources properly. diff --git a/examples/03-dependencies/score.yaml b/examples/03-dependencies/score.yaml index adb3e66..02ea32b 100644 --- a/examples/03-dependencies/score.yaml +++ b/examples/03-dependencies/score.yaml @@ -15,23 +15,7 @@ containers: resources: db: type: postgres - properties: - host: - default: localhost - port: - default: 5432 - name: - default: postgres - user: - secret: true - password: - secret: true dns: type: dns - properties: - domain: backend: type: service - properties: - name: - port: diff --git a/examples/04-extras/README.md b/examples/04-extras/README.md index 91dcc70..3c2be12 100644 --- a/examples/04-extras/README.md +++ b/examples/04-extras/README.md @@ -26,11 +26,6 @@ containers: resources: env: type: environment - properties: - MESSAGE: - type: string - DATADOG_ENV: - type: string dns: type: dns ``` diff --git a/examples/04-extras/score.yaml b/examples/04-extras/score.yaml index ed5b068..9edcabd 100644 --- a/examples/04-extras/score.yaml +++ b/examples/04-extras/score.yaml @@ -19,10 +19,5 @@ containers: resources: env: type: environment - properties: - MESSAGE: - type: string - DATADOG_ENV: - type: string dns: type: dns diff --git a/go.mod b/go.mod index 4bb71da..c25e930 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/golang/mock 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-20230601114155-58fa99cb56f8 + github.com/score-spec/score-go v0.0.0-20230615134243-75a810d22ad1 github.com/sendgrid/rest v2.6.9+incompatible github.com/spf13/cobra v1.6.0 github.com/stretchr/testify v1.8.1 diff --git a/go.sum b/go.sum index 79e7ca1..88a3149 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/score-spec/score-go v0.0.0-20230601114155-58fa99cb56f8 h1:gIOGix8DrqtGbEEBPimUON83Bk+AVnZRpanCKsWXa3s= -github.com/score-spec/score-go v0.0.0-20230601114155-58fa99cb56f8/go.mod h1:kqDzGrkDasa4D1A9MWgHPVPoRVa+zZgFijYOZNDLSpM= +github.com/score-spec/score-go v0.0.0-20230615134243-75a810d22ad1 h1:i/6Z1cPwKOEJp0jNK3oyh1RCWnGR1Cn4cAorRdCIe2M= +github.com/score-spec/score-go v0.0.0-20230615134243-75a810d22ad1/go.mod h1:kqDzGrkDasa4D1A9MWgHPVPoRVa+zZgFijYOZNDLSpM= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= diff --git a/internal/humanitec/convert.go b/internal/humanitec/convert.go index 5378ceb..0407963 100644 --- a/internal/humanitec/convert.go +++ b/internal/humanitec/convert.go @@ -144,14 +144,14 @@ func convertContainerSpec(name string, spec *score.ContainerSpec, context *templ // ConvertSpec converts SCORE specification into Humanitec deployment delta. func ConvertSpec(name, envID string, spec *score.WorkloadSpec, ext *extensions.HumanitecExtensionsSpec) (*humanitec.CreateDeploymentDeltaRequest, error) { - context, err := buildContext(spec.Metadata, spec.Resources, ext.Resources) + ctx, err := buildContext(spec.Metadata, spec.Resources, ext.Resources) if err != nil { return nil, fmt.Errorf("preparing context: %w", err) } var containers = make(map[string]interface{}, len(spec.Containers)) for cName, cSpec := range spec.Containers { - if container, err := convertContainerSpec(cName, &cSpec, &context); err == nil { + if container, err := convertContainerSpec(cName, &cSpec, ctx); err == nil { containers[cName] = container } else { return nil, fmt.Errorf("processing container specification for '%s': %w", cName, err) @@ -184,7 +184,7 @@ func ConvertSpec(name, envID string, spec *score.WorkloadSpec, ext *extensions.H } if ext != nil && len(ext.Spec) > 0 { - var features = context.SubstituteAll(ext.Spec) + var features = ctx.SubstituteAll(ext.Spec) if err := mergo.Merge(&workloadSpec, features); err != nil { return nil, fmt.Errorf("applying workload profile features: %w", err) } diff --git a/internal/humanitec/convert_test.go b/internal/humanitec/convert_test.go index 1e11064..2de91b6 100644 --- a/internal/humanitec/convert_test.go +++ b/internal/humanitec/convert_test.go @@ -279,16 +279,9 @@ func TestScoreConvert(t *testing.T) { }, }, Type: "environment", - Properties: map[string]score.ResourcePropertySpec{ - "DEBUG": {Default: false, Required: false}, - "DATADOG_ENV": {}, - }, }, "dns": { Type: "dns", - Properties: map[string]score.ResourcePropertySpec{ - "domain": {}, - }, Params: map[string]interface{}{ "test": "value", }, @@ -303,13 +296,6 @@ func TestScoreConvert(t *testing.T) { }, }, Type: "postgres", - Properties: map[string]score.ResourcePropertySpec{ - "host": {Default: "localhost", Required: true}, - "port": {Default: 5432, Required: false}, - "name": {Required: true}, - "user_name": {Required: true, Secret: true}, - "password": {Required: true, Secret: true}, - }, Params: map[string]interface{}{ "extensions": map[string]interface{}{ "uuid-ossp": map[string]interface{}{ @@ -321,10 +307,6 @@ func TestScoreConvert(t *testing.T) { }, "orders": { Type: "service", - Properties: map[string]score.ResourcePropertySpec{ - "name": {Required: false}, - "port": {}, - }, }, "external-resource": { Metadata: score.ResourceMeta{ @@ -333,9 +315,6 @@ func TestScoreConvert(t *testing.T) { }, }, Type: "some-type", - Properties: map[string]score.ResourcePropertySpec{ - "name": {Required: false}, - }, }, }, }, diff --git a/internal/humanitec/templates.go b/internal/humanitec/templates.go index 1cb195a..30d401a 100644 --- a/internal/humanitec/templates.go +++ b/internal/humanitec/templates.go @@ -11,6 +11,7 @@ import ( "fmt" "log" "os" + "strings" "github.com/mitchellh/mapstructure" @@ -19,12 +20,14 @@ import ( ) // templatesContext ia an utility type that provides a context for '${...}' templates substitution -type templatesContext map[string]string +type templatesContext struct { + meta map[string]interface{} + resources score.ResourcesSpecs + extensions extensions.HumanitecResourcesSpecs +} // buildContext initializes a new templatesContext instance -func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs, ext extensions.HumanitecResourcesSpecs) (templatesContext, error) { - var ctx = make(map[string]string) - +func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs, ext extensions.HumanitecResourcesSpecs) (*templatesContext, error) { var metadataMap = make(map[string]interface{}) if decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ TagName: "json", @@ -33,75 +36,26 @@ func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs, e 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 { - var source string - switch res.Type { - case "environment": - source = "values" - case "service": - source = fmt.Sprintf("modules.%s", resName) - default: - if res.Type == "workload" { - log.Println("Warning: 'workload' is a reserved resource type. Its usage may lead to compatibility issues with future releases of this application.") - } - resId, hasAnnotation := res.Metadata.Annotations[AnnotationLabelResourceId] - // DEPRECATED: Should use resource annotations instead - if resExt, hasMeta := ext[resName]; hasMeta && !hasAnnotation { - if resExt.Scope == "" || resExt.Scope == "external" { - resId = fmt.Sprintf("externals.%s", resName) - } else if resExt.Scope == "shared" { - resId = fmt.Sprintf("shared.%s", resName) - } - } - // END (DEPRECATED) - - if resId != "" { - source = resId - } else { - source = fmt.Sprintf("externals.%s", resName) - } - } - ctx[fmt.Sprintf("resources.%s", resName)] = source - - for propName := range res.Properties { - var ref = fmt.Sprintf("resources.%s.%s", resName, propName) - if _, exists := ctx[ref]; exists { - return nil, fmt.Errorf("ambiguous property reference '%s'", ref) - } - var sourceProp string - switch res.Type { - case "service": - sourceProp = fmt.Sprintf("service.%s", propName) - default: - sourceProp = propName - } - ctx[ref] = fmt.Sprintf("${%s.%s}", source, sourceProp) - } } - return ctx, nil + return &templatesContext{ + meta: metadataMap, + resources: resources, + extensions: ext, + }, nil } // SubstituteAll replaces all matching '${...}' templates in map keys and string values recursively. -func (context templatesContext) SubstituteAll(src map[string]interface{}) map[string]interface{} { +func (ctx *templatesContext) SubstituteAll(src map[string]interface{}) map[string]interface{} { var dst = make(map[string]interface{}, 0) for key, val := range src { - key = context.Substitute(key) + key = ctx.Substitute(key) switch v := val.(type) { case string: - val = context.Substitute(v) + val = ctx.Substitute(v) case map[string]interface{}: - val = context.SubstituteAll(v) + val = ctx.SubstituteAll(v) } dst[key] = val } @@ -110,13 +64,13 @@ func (context templatesContext) SubstituteAll(src map[string]interface{}) map[st } // Substitute replaces all matching '${...}' templates in a source string -func (context templatesContext) Substitute(src string) string { - return os.Expand(src, context.mapVar) +func (ctx *templatesContext) Substitute(src string) string { + return os.Expand(src, ctx.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 { +func (ctx *templatesContext) mapVar(ref string) string { if ref == "" { return "" } @@ -130,8 +84,63 @@ func (context templatesContext) mapVar(ref string) string { return ref } - if res, ok := context[ref]; ok { - return res + var segments = strings.SplitN(ref, ".", 2) + switch segments[0] { + case "metadata": + if len(segments) == 2 { + if val, exists := ctx.meta[segments[1]]; exists { + return fmt.Sprintf("%v", val) + } + } + + case "resources": + if len(segments) == 2 { + segments = strings.SplitN(segments[1], ".", 2) + var resName = segments[0] + if res, exists := ctx.resources[resName]; exists { + var source string + switch res.Type { + case "environment": + source = "values" + case "service": + source = fmt.Sprintf("modules.%s", resName) + default: + if res.Type == "workload" { + log.Println("Warning: 'workload' is a reserved resource type. Its usage may lead to compatibility issues with future releases of this application.") + } + resId, hasAnnotation := res.Metadata.Annotations[AnnotationLabelResourceId] + // DEPRECATED: Should use resource annotations instead + if resExt, hasMeta := ctx.extensions[resName]; hasMeta && !hasAnnotation { + if resExt.Scope == "" || resExt.Scope == "external" { + resId = fmt.Sprintf("externals.%s", resName) + } else if resExt.Scope == "shared" { + resId = fmt.Sprintf("shared.%s", resName) + } + } + // END (DEPRECATED) + + if resId != "" { + source = resId + } else { + source = fmt.Sprintf("externals.%s", resName) + } + } + + if len(segments) == 1 { + return source + } else { + var propName = segments[1] + var sourceProp string + switch res.Type { + case "service": + sourceProp = fmt.Sprintf("service.%s", propName) + default: + sourceProp = propName + } + return fmt.Sprintf("${%s.%s}", source, sourceProp) + } + } + } } log.Printf("Warning: Can not resolve '%s'. Resource or property is not declared.", ref) diff --git a/internal/humanitec/templates_test.go b/internal/humanitec/templates_test.go index 0e10c23..f2a303c 100644 --- a/internal/humanitec/templates_test.go +++ b/internal/humanitec/templates_test.go @@ -16,7 +16,7 @@ import ( extensions "github.com/score-spec/score-humanitec/internal/humanitec/extensions" ) -func TestBuildContext(t *testing.T) { +func TestMapVar(t *testing.T) { var meta = score.WorkloadMeta{ Name: "test-name", } @@ -24,30 +24,15 @@ func TestBuildContext(t *testing.T) { var resources = score.ResourcesSpecs{ "env": score.ResourceSpec{ Type: "environment", - Properties: map[string]score.ResourcePropertySpec{ - "DEBUG": {Required: false, Default: true}, - }, }, "db": score.ResourceSpec{ Type: "postgres", - Properties: map[string]score.ResourcePropertySpec{ - "host": {Required: true, Default: "."}, - "port": {Required: true, Default: "5342"}, - "name": {Required: true}, - }, }, "dns": score.ResourceSpec{ Type: "dns", - Properties: map[string]score.ResourcePropertySpec{ - "domain": {}, - }, }, "service-a": score.ResourceSpec{ Type: "service", - Properties: map[string]score.ResourcePropertySpec{ - "name": {}, - "port": {}, - }, }, } @@ -55,126 +40,100 @@ func TestBuildContext(t *testing.T) { "dns": {Scope: "shared"}, } - context, err := buildContext(meta, resources, ext) + ctx, err := buildContext(meta, resources, ext) assert.NoError(t, err) - assert.Equal(t, templatesContext{ - "metadata.name": "test-name", - - "resources.env": "values", - "resources.env.DEBUG": "${values.DEBUG}", - - "resources.db": "externals.db", - "resources.db.host": "${externals.db.host}", - "resources.db.port": "${externals.db.port}", - "resources.db.name": "${externals.db.name}", - - "resources.dns": "shared.dns", - "resources.dns.domain": "${shared.dns.domain}", - - "resources.service-a": "modules.service-a", - "resources.service-a.name": "${modules.service-a.service.name}", - "resources.service-a.port": "${modules.service-a.service.port}", - }, context) -} - -func TestMapVar(t *testing.T) { - var context = templatesContext{ - "metadata.name": "test-name", - - "resources.env": "values", - "resources.env.DEBUG": "${values.DEBUG}", - - "resources.db": "externals.db", - "resources.db.host": "${externals.db.host}", - "resources.db.port": "${externals.db.port}", - "resources.db.name": "${externals.db.name}", - - "resources.dns": "shared.dns", - "resources.dns.domain": "${shared.dns.domain}", - - "resources.service-a": "modules.service-a", - "resources.service-a.name": "${modules.service-a.service.name}", - "resources.service-a.port": "${modules.service-a.service.port}", - } - - assert.Equal(t, "", context.mapVar("")) - assert.Equal(t, "$", context.mapVar("$")) - - assert.Equal(t, "test-name", context.mapVar("metadata.name")) - assert.Equal(t, "${metadata.name.nil}", context.mapVar("metadata.name.nil")) - assert.Equal(t, "${metadata.nil}", context.mapVar("metadata.nil")) - - assert.Equal(t, "${values.DEBUG}", context.mapVar("resources.env.DEBUG")) - - assert.Equal(t, "externals.db", context.mapVar("resources.db")) - assert.Equal(t, "${externals.db.host}", context.mapVar("resources.db.host")) - assert.Equal(t, "${externals.db.port}", context.mapVar("resources.db.port")) - assert.Equal(t, "${externals.db.name}", context.mapVar("resources.db.name")) - assert.Equal(t, "${resources.db.name.nil}", context.mapVar("resources.db.name.nil")) - assert.Equal(t, "${resources.db.nil}", context.mapVar("resources.db.nil")) - assert.Equal(t, "${modules.service-a.service.name}", context.mapVar("resources.service-a.name")) - assert.Equal(t, "${modules.service-a.service.port}", context.mapVar("resources.service-a.port")) - assert.Equal(t, "${resources.nil}", context.mapVar("resources.nil")) - assert.Equal(t, "${nil.db.name}", context.mapVar("nil.db.name")) + assert.Equal(t, "", ctx.mapVar("")) + assert.Equal(t, "$", ctx.mapVar("$")) + + assert.Equal(t, "test-name", ctx.mapVar("metadata.name")) + assert.Equal(t, "${metadata.name.nil}", ctx.mapVar("metadata.name.nil")) + assert.Equal(t, "${metadata.nil}", ctx.mapVar("metadata.nil")) + + assert.Equal(t, "${values.DEBUG}", ctx.mapVar("resources.env.DEBUG")) + + assert.Equal(t, "externals.db", ctx.mapVar("resources.db")) + assert.Equal(t, "${externals.db.host}", ctx.mapVar("resources.db.host")) + assert.Equal(t, "${externals.db.port}", ctx.mapVar("resources.db.port")) + assert.Equal(t, "${externals.db.name}", ctx.mapVar("resources.db.name")) + assert.Equal(t, "${externals.db.name.nil}", ctx.mapVar("resources.db.name.nil")) + assert.Equal(t, "${externals.db.nil}", ctx.mapVar("resources.db.nil")) + assert.Equal(t, "${modules.service-a.service.name}", ctx.mapVar("resources.service-a.name")) + assert.Equal(t, "${modules.service-a.service.port}", ctx.mapVar("resources.service-a.port")) + assert.Equal(t, "${resources.nil}", ctx.mapVar("resources.nil")) + assert.Equal(t, "${nil.db.name}", ctx.mapVar("nil.db.name")) } func TestSubstitute(t *testing.T) { - var context = templatesContext{ - "metadata.name": "test-name", - - "resources.env": "values", - "resources.env.DEBUG": "${values.DEBUG}", - - "resources.db": "externals.db", - "resources.db.host": "${externals.db.host}", - "resources.db.port": "${externals.db.port}", - "resources.db.name": "${externals.db.name}", + var meta = score.WorkloadMeta{ + Name: "test-name", + } - "resources.dns": "shared.dns", - "resources.dns.domain": "${shared.dns.domain}", + var resources = score.ResourcesSpecs{ + "env": score.ResourceSpec{ + Type: "environment", + }, + "db": score.ResourceSpec{ + Type: "postgres", + }, + "dns": score.ResourceSpec{ + Type: "dns", + }, + "service-a": score.ResourceSpec{ + Type: "service", + }, + } - "resources.service-a": "modules.service-a", - "resources.service-a.name": "${modules.service-a.service.name}", - "resources.service-a.port": "${modules.service-a.service.port}", + var ext = extensions.HumanitecResourcesSpecs{ + "dns": {Scope: "shared"}, } - assert.Equal(t, "", context.Substitute("")) - assert.Equal(t, "abc", context.Substitute("abc")) - assert.Equal(t, "abc $ abc", context.Substitute("abc $$ abc")) - assert.Equal(t, "${abc}", context.Substitute("$${abc}")) + ctx, err := buildContext(meta, resources, ext) + assert.NoError(t, err) + + assert.Equal(t, "", ctx.Substitute("")) + assert.Equal(t, "abc", ctx.Substitute("abc")) + assert.Equal(t, "abc $ abc", ctx.Substitute("abc $$ abc")) + assert.Equal(t, "${abc}", ctx.Substitute("$${abc}")) - assert.Equal(t, "The name is 'test-name'", context.Substitute("The name is '${metadata.name}'")) - assert.Equal(t, "The name is '${metadata.nil}'", context.Substitute("The name is '${metadata.nil}'")) + assert.Equal(t, "The name is 'test-name'", ctx.Substitute("The name is '${metadata.name}'")) + assert.Equal(t, "The name is '${metadata.nil}'", ctx.Substitute("The name is '${metadata.nil}'")) - assert.Equal(t, "resources.env.DEBUG", context.Substitute("resources.env.DEBUG")) + assert.Equal(t, "resources.env.DEBUG", ctx.Substitute("resources.env.DEBUG")) - assert.Equal(t, "externals.db", context.Substitute("${resources.db}")) + assert.Equal(t, "externals.db", ctx.Substitute("${resources.db}")) assert.Equal(t, - "postgresql://${resources.db.user}:${resources.db.password}@${externals.db.host}:${externals.db.port}/${externals.db.name}", - context.Substitute("postgresql://${resources.db.user}:${resources.db.password}@${resources.db.host}:${resources.db.port}/${resources.db.name}")) + "postgresql://${externals.db.user}:${externals.db.password}@${externals.db.host}:${externals.db.port}/${externals.db.name}", + ctx.Substitute("postgresql://${resources.db.user}:${resources.db.password}@${resources.db.host}:${resources.db.port}/${resources.db.name}")) } func TestSubstituteAll(t *testing.T) { - var context = templatesContext{ - "metadata.name": "test-name", - - "resources.env": "values", - "resources.env.DEBUG": "${values.DEBUG}", - - "resources.db": "externals.db", - "resources.db.host": "${externals.db.host}", - "resources.db.port": "${externals.db.port}", - "resources.db.name": "${externals.db.name}", + var meta = score.WorkloadMeta{ + Name: "test-name", + } - "resources.dns": "shared.dns", - "resources.dns.domain": "${shared.dns.domain}", + var resources = score.ResourcesSpecs{ + "env": score.ResourceSpec{ + Type: "environment", + }, + "db": score.ResourceSpec{ + Type: "postgres", + }, + "dns": score.ResourceSpec{ + Type: "dns", + }, + "service-a": score.ResourceSpec{ + Type: "service", + }, + } - "resources.service-a": "modules.service-a", - "resources.service-a.name": "${modules.service-a.service.name}", - "resources.service-a.port": "${modules.service-a.service.port}", + var ext = extensions.HumanitecResourcesSpecs{ + "dns": {Scope: "shared"}, } + ctx, err := buildContext(meta, resources, ext) + assert.NoError(t, err) + var source = map[string]interface{}{ "api": map[string]interface{}{ "${resources.service-a.name}": map[string]interface{}{ @@ -197,5 +156,5 @@ func TestSubstituteAll(t *testing.T) { "DEBUG": "${values.DEBUG}", } - assert.Equal(t, expected, context.SubstituteAll(source)) + assert.Equal(t, expected, ctx.SubstituteAll(source)) }