diff --git a/cmd/score-helm/main.go b/cmd/score-helm/main.go index 8ea003d..c9c6da2 100644 --- a/cmd/score-helm/main.go +++ b/cmd/score-helm/main.go @@ -17,7 +17,7 @@ import ( func main() { if err := command.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, "Error: "+err.Error()) os.Exit(1) } } diff --git a/go.mod b/go.mod index 8093062..0db2cd5 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,13 @@ module github.com/score-spec/score-helm -go 1.19 +go 1.22 + +toolchain go1.22.0 require ( github.com/imdario/mergo v0.3.13 github.com/mitchellh/mapstructure v1.5.0 - github.com/score-spec/score-go v0.0.0-20230905115428-131acdd2f5cf + github.com/score-spec/score-go v1.1.0 github.com/spf13/cobra v1.6.0 github.com/stretchr/testify v1.8.0 github.com/tidwall/sjson v1.2.5 diff --git a/go.sum b/go.sum index 11b33ec..8e5a062 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/score-spec/score-go v0.0.0-20230905115428-131acdd2f5cf h1:0Dt+qyYoGTXPPU5Xq/KxLDtQMMA8hX+oJ8EsjFWUajQ= -github.com/score-spec/score-go v0.0.0-20230905115428-131acdd2f5cf/go.mod h1:3l9mvrtYKzxXDQVcYkQBD3ABTPkTzWhUMYNfGlpctoo= +github.com/score-spec/score-go v1.1.0 h1:63WM1u93NtGgMuPtVZ/UBfzg/BpYuY8sBquaL0BkrXU= +github.com/score-spec/score-go v1.1.0/go.mod h1:nt6TOq2Ld9SiH3Fd9NF8tiJ9L7S17OE3FNgCrSet5GQ= github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/internal/command/root.go b/internal/command/root.go index 4504032..d0a594c 100644 --- a/internal/command/root.go +++ b/internal/command/root.go @@ -10,8 +10,9 @@ package command import ( "fmt" - "github.com/score-spec/score-helm/internal/version" "github.com/spf13/cobra" + + "github.com/score-spec/score-helm/internal/version" ) var ( @@ -21,7 +22,8 @@ var ( Long: `SCORE is a specification for defining environment agnostic configuration for cloud based workloads. This tool produces a Helm chart from the SCORE specification. Complete documentation is available at https://score.dev.`, - Version: fmt.Sprintf("%s (build: %s; sha: %s)", version.Version, version.BuildTime, version.GitSHA), + Version: fmt.Sprintf("%s (build: %s; sha: %s)", version.Version, version.BuildTime, version.GitSHA), + SilenceErrors: true, } ) diff --git a/internal/command/run.go b/internal/command/run.go index 634d5e2..b786b8c 100644 --- a/internal/command/run.go +++ b/internal/command/run.go @@ -24,6 +24,7 @@ import ( loader "github.com/score-spec/score-go/loader" schema "github.com/score-spec/score-go/schema" score "github.com/score-spec/score-go/types" + helm "github.com/score-spec/score-helm/internal/helm" ) @@ -62,9 +63,14 @@ var runCmd = &cobra.Command{ Use: "run", Short: "Translate SCORE file into Helm values file", RunE: run, + // we print errors ourselves at the top level + SilenceErrors: true, } func run(cmd *cobra.Command, args []string) error { + // don't print usage if we've parsed the args successfully + cmd.SilenceUsage = true + if !verbose { log.SetOutput(io.Discard) } @@ -158,6 +164,15 @@ func run(cmd *cobra.Command, args []string) error { } } + // Apply upgrades to fix backports or backward incompatible things + if changes, err := schema.ApplyCommonUpgradeTransforms(srcMap); err != nil { + return fmt.Errorf("failed to upgrade spec: %w", err) + } else if len(changes) > 0 { + for _, change := range changes { + log.Printf("Applying upgrade to specification: %s\n", change) + } + } + // Validate SCORE spec // if !skipValidation { @@ -169,7 +184,7 @@ func run(cmd *cobra.Command, args []string) error { // Convert SCORE spec // - var spec score.WorkloadSpec + var spec score.Workload log.Print("Validating SCORE spec...\n") if err = mapstructure.Decode(srcMap, &spec); err != nil { return fmt.Errorf("validating workload spec: %w", err) diff --git a/internal/helm/convert.go b/internal/helm/convert.go index 72ff2ca..e0e6670 100644 --- a/internal/helm/convert.go +++ b/internal/helm/convert.go @@ -16,21 +16,23 @@ import ( // getProbeDetails extracts an httpGet probe details from the source spec. // Returns nil if the source spec is empty. -func getProbeDetails(probe *score.ContainerProbeSpec) map[string]interface{} { - if probe.HTTPGet.Path == "" { +func getProbeDetails(probe *score.ContainerProbe) map[string]interface{} { + if probe.HttpGet.Path == "" { return nil } var res = map[string]interface{}{ "type": "http", - "path": probe.HTTPGet.Path, - "port": probe.HTTPGet.Port, + "path": probe.HttpGet.Path, + "port": probe.HttpGet.Port, } - if len(probe.HTTPGet.HTTPHeaders) > 0 { + if len(probe.HttpGet.HttpHeaders) > 0 { var hdrs = map[string]string{} - for _, hdr := range probe.HTTPGet.HTTPHeaders { - hdrs[hdr.Name] = hdr.Value + for _, hdr := range probe.HttpGet.HttpHeaders { + if hdr.Name != nil && hdr.Value != nil { + hdrs[*hdr.Name] = *hdr.Value + } } res["httpHeaders"] = hdrs } @@ -39,7 +41,7 @@ func getProbeDetails(probe *score.ContainerProbeSpec) map[string]interface{} { } // ConvertSpec converts SCORE specification into Helm values map. -func ConvertSpec(dest map[string]interface{}, spec *score.WorkloadSpec, values map[string]interface{}) error { +func ConvertSpec(dest map[string]interface{}, spec *score.Workload, values map[string]interface{}) error { if values == nil { values = make(map[string]interface{}) } @@ -48,18 +50,18 @@ func ConvertSpec(dest map[string]interface{}, spec *score.WorkloadSpec, values m return fmt.Errorf("preparing context: %w", err) } - if len(spec.Service.Ports) > 0 { + if spec.Service != nil && len(spec.Service.Ports) > 0 { var ports = make([]interface{}, 0, len(spec.Service.Ports)) for name, port := range spec.Service.Ports { var pVals = map[string]interface{}{ "name": name, "port": port.Port, } - if port.Protocol != "" { - pVals["protocol"] = port.Protocol + if port.Protocol != nil { + pVals["protocol"] = string(*port.Protocol) } - if port.TargetPort > 0 { - pVals["targetPort"] = port.TargetPort + if port.TargetPort != nil { + pVals["targetPort"] = *port.TargetPort } ports = append(ports, pVals) } @@ -110,32 +112,56 @@ func ConvertSpec(dest map[string]interface{}, spec *score.WorkloadSpec, values m var source = context.Substitute(vol.Source) var vVals = map[string]interface{}{ "name": source, - "subPath": vol.Path, "mountPath": vol.Target, - "readOnly": vol.ReadOnly, + } + if vol.Path != nil { + vVals["subPath"] = *vol.Path + } + if vol.ReadOnly != nil { + vVals["readOnly"] = *vol.ReadOnly } volumes = append(volumes, vVals) } cVals["volumeMounts"] = volumes } - if probe := getProbeDetails(&cSpec.LivenessProbe); len(probe) > 0 { - cVals["livenessProbe"] = probe + if cSpec.LivenessProbe != nil { + if probe := getProbeDetails(cSpec.LivenessProbe); len(probe) > 0 { + cVals["livenessProbe"] = probe + } } - if probe := getProbeDetails(&cSpec.ReadinessProbe); len(probe) > 0 { - cVals["readinessProbe"] = probe + if cSpec.ReadinessProbe != nil { + if probe := getProbeDetails(cSpec.ReadinessProbe); len(probe) > 0 { + cVals["readinessProbe"] = probe + } } - if len(cSpec.Resources.Requests) > 0 || len(cSpec.Resources.Limits) > 0 { - cVals["resources"] = map[string]interface{}{ - "requests": cSpec.Resources.Requests, - "limits": cSpec.Resources.Limits, + if cSpec.Resources != nil { + containerResources := make(map[string]interface{}) + if out := getContainerResources(cSpec.Resources.Limits); len(out) > 0 { + containerResources["limits"] = out + } + if out := getContainerResources(cSpec.Resources.Requests); len(out) > 0 { + containerResources["requests"] = out + } + if len(containerResources) > 0 { + cVals["resources"] = containerResources } } - containers[name] = cVals } dest["containers"] = containers return nil } + +func getContainerResources(requests *score.ResourcesLimits) map[string]interface{} { + out := make(map[string]interface{}) + if requests.Cpu != nil { + out["cpu"] = *requests.Cpu + } + if requests.Memory != nil { + out["memory"] = *requests.Memory + } + return out +} diff --git a/internal/helm/convert_test.go b/internal/helm/convert_test.go index a34d75d..406636c 100644 --- a/internal/helm/convert_test.go +++ b/internal/helm/convert_test.go @@ -17,7 +17,7 @@ import ( func TestScoreConvert(t *testing.T) { var tests = []struct { Name string - Spec *score.WorkloadSpec + Spec *score.Workload Values map[string]interface{} Expected map[string]interface{} Error error @@ -26,24 +26,24 @@ func TestScoreConvert(t *testing.T) { // { Name: "Should convert SCORE to Helm values", - Spec: &score.WorkloadSpec{ - Metadata: score.WorkloadMeta{ - Name: "test", + Spec: &score.Workload{ + Metadata: score.WorkloadMetadata{ + "name": "test", }, - Service: score.ServiceSpec{ - Ports: score.ServicePortsSpecs{ - "www": score.ServicePortSpec{ + Service: &score.WorkloadService{ + Ports: score.WorkloadServicePorts{ + "www": score.ServicePort{ Port: 80, - TargetPort: 8080, + TargetPort: Ref(8080), }, - "admin": score.ServicePortSpec{ + "admin": score.ServicePort{ Port: 8080, - Protocol: "UDP", + Protocol: Ref(score.ServicePortProtocolUDP), }, }, }, - Containers: score.ContainersSpecs{ - "backend": score.ContainerSpec{ + Containers: score.WorkloadContainers{ + "backend": score.Container{ Image: "busybox", Command: []string{ "/bin/sh", @@ -97,12 +97,12 @@ func TestScoreConvert(t *testing.T) { }, { Name: "Should convert all resources references", - Spec: &score.WorkloadSpec{ - Metadata: score.WorkloadMeta{ - Name: "test", + Spec: &score.Workload{ + Metadata: score.WorkloadMetadata{ + "name": "test", }, - Containers: score.ContainersSpecs{ - "backend": score.ContainerSpec{ + Containers: score.WorkloadContainers{ + "backend": score.Container{ Image: "busybox", Variables: map[string]string{ "DEBUG": "${resources.env.DEBUG}", @@ -110,17 +110,17 @@ func TestScoreConvert(t *testing.T) { "DOMAIN_NAME": "${resources.dns.domain_name}", "CONNECTION_STRING": "postgresql://${resources.app-db.host}:${resources.app-db.port}/${resources.app-db.name}", }, - Volumes: []score.VolumeMountSpec{ + Volumes: []score.ContainerVolumesElem{ { Source: "${resources.data}", - Path: "sub/path", + Path: Ref("sub/path"), Target: "/mnt/data", - ReadOnly: true, + ReadOnly: Ref(true), }, }, }, }, - Resources: map[string]score.ResourceSpec{ + Resources: score.WorkloadResources{ "env": { Type: "environment", }, diff --git a/internal/helm/ref.go b/internal/helm/ref.go new file mode 100644 index 0000000..70f65f1 --- /dev/null +++ b/internal/helm/ref.go @@ -0,0 +1,5 @@ +package helm + +func Ref[k any](input k) *k { + return &input +} diff --git a/internal/helm/templates.go b/internal/helm/templates.go index 5b22c79..e6061df 100644 --- a/internal/helm/templates.go +++ b/internal/helm/templates.go @@ -25,12 +25,12 @@ var ( // templatesContext ia an utility type that provides a context for '${...}' templates substitution type templatesContext struct { meta map[string]interface{} - resources score.ResourcesSpecs + resources score.WorkloadResources values map[string]interface{} } // buildContext initializes a new templatesContext instance -func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs, values map[string]interface{}) (*templatesContext, error) { +func buildContext(metadata score.WorkloadMetadata, resources score.WorkloadResources, values map[string]interface{}) (*templatesContext, error) { var metadataMap = make(map[string]interface{}) if decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ TagName: "json", diff --git a/internal/helm/templates_test.go b/internal/helm/templates_test.go index d2133ef..e156b93 100644 --- a/internal/helm/templates_test.go +++ b/internal/helm/templates_test.go @@ -15,18 +15,18 @@ import ( ) func TestMapVar(t *testing.T) { - var meta = score.WorkloadMeta{ - Name: "test-name", + var meta = score.WorkloadMetadata{ + "name": "test-name", } - var resources = score.ResourcesSpecs{ - "env": score.ResourceSpec{ + var resources = score.WorkloadResources{ + "env": score.Resource{ Type: "environment", }, - "db": score.ResourceSpec{ + "db": score.Resource{ Type: "postgres", }, - "dns": score.ResourceSpec{ + "dns": score.Resource{ Type: "dns", }, } @@ -64,18 +64,18 @@ func TestMapVar(t *testing.T) { } func TestSubstitute(t *testing.T) { - var meta = score.WorkloadMeta{ - Name: "test-name", + var meta = score.WorkloadMetadata{ + "name": "test-name", } - var resources = score.ResourcesSpecs{ - "env": score.ResourceSpec{ + var resources = score.WorkloadResources{ + "env": score.Resource{ Type: "environment", }, - "db": score.ResourceSpec{ + "db": score.Resource{ Type: "postgres", }, - "dns": score.ResourceSpec{ + "dns": score.Resource{ Type: "dns", }, }