├── examples ├── 02-environment │ ├── env.yaml │ ├── score.yaml │ └── README.md ├── chart │ ├── Chart.yaml │ ├── templates │ │ ├── service.yaml │ │ ├── _helpers.tpl │ │ └── deployment.yaml │ ├── .helmignore │ └── values.yaml └── 01-hello │ ├── score.yaml │ └── README.md ├── docs └── images │ ├── banner.png │ ├── install.svg │ ├── contributing.svg │ ├── overview.svg │ ├── get-involved.svg │ └── logo.svg ├── internal ├── helm │ ├── ref.go │ ├── write.go │ ├── templates.go │ ├── templates_test.go │ ├── write_test.go │ ├── convert.go │ └── convert_test.go ├── version │ └── version.go └── command │ ├── root.go │ └── run.go ├── .gitignore ├── cmd └── score-helm │ └── main.go ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── workflows │ └── release.yaml ├── go.mod ├── Dockerfile ├── .goreleaser.yaml ├── README.md ├── go.sum ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── LICENSE /examples/02-environment/env.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | NAME: John -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/score-spec/score-helm/HEAD/docs/images/banner.png -------------------------------------------------------------------------------- /internal/helm/ref.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | func Ref[k any](input k) *k { 4 | return &input 5 | } 6 | -------------------------------------------------------------------------------- /examples/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # This file is replaced (generated) during the Helm chart scaffolding. 3 | # 4 | apiVersion: v2 5 | name: me-helm 6 | description: A Helm chart for Kubernetes 7 | type: application 8 | version: "0.0.0" 9 | appVersion: "0.0.0" -------------------------------------------------------------------------------- /examples/01-hello/score.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: score.dev/v1b1 2 | 3 | metadata: 4 | name: hello-world 5 | 6 | containers: 7 | hello: 8 | image: busybox 9 | command: ["/bin/sh"] 10 | args: ["-c", "while true; do echo Hello World!; sleep 5; done"] 11 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package version 9 | 10 | var ( 11 | Version string = "0.0.0" 12 | BuildTime string = "local" 13 | GitSHA string = "dirty" 14 | ) 15 | -------------------------------------------------------------------------------- /examples/02-environment/score.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: score.dev/v1b1 2 | metadata: 3 | name: hello-world 4 | containers: 5 | hello: 6 | image: busybox 7 | command: ["/bin/sh"] 8 | args: ["-c", "while true; do echo Hello $${FRIEND}!; sleep 5; done"] 9 | variables: 10 | FRIEND: ${resources.env.NAME} 11 | resources: 12 | env: 13 | type: environment 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VS Code workspace configuration 2 | .vscode/ 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | -------------------------------------------------------------------------------- /examples/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | {{- with .Values.service }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include ".fullname" . }} 6 | labels: 7 | {{- include ".labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .type }} 10 | selector: 11 | {{- include ".selectorLabels" . | nindent 4 }} 12 | {{- with .ports }} 13 | ports: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | {{- end }} -------------------------------------------------------------------------------- /examples/chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ -------------------------------------------------------------------------------- /cmd/score-helm/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/score-spec/score-helm/internal/command" 15 | ) 16 | 17 | func main() { 18 | 19 | if err := command.Execute(); err != nil { 20 | _, _ = fmt.Fprintln(os.Stderr, "Error: "+err.Error()) 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/helm/write.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package helm 9 | 10 | import ( 11 | "io" 12 | 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | // WriteYAML exports Helm values into YAML. 17 | func WriteYAML(w io.Writer, values map[string]interface{}) error { 18 | enc := yaml.NewEncoder(w) 19 | enc.SetIndent(2) 20 | return enc.Encode(values) 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature request]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/images/install.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Version [e.g. 1.1.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /docs/images/contributing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/overview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/get-involved.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/score-spec/score-helm 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.0 6 | 7 | require ( 8 | github.com/imdario/mergo v0.3.13 9 | github.com/mitchellh/mapstructure v1.5.0 10 | github.com/score-spec/score-go v1.1.0 11 | github.com/spf13/cobra v1.6.0 12 | github.com/stretchr/testify v1.8.0 13 | github.com/tidwall/sjson v1.2.5 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 20 | github.com/kr/pretty v0.1.0 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect 23 | github.com/spf13/pflag v1.0.5 // indirect 24 | github.com/tidwall/gjson v1.14.2 // indirect 25 | github.com/tidwall/match v1.1.1 // indirect 26 | github.com/tidwall/pretty v1.2.0 // indirect 27 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /internal/command/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package command 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/spf13/cobra" 14 | 15 | "github.com/score-spec/score-helm/internal/version" 16 | ) 17 | 18 | var ( 19 | rootCmd = &cobra.Command{ 20 | Use: "score-helm", 21 | Short: "SCORE to Helm translator", 22 | Long: `SCORE is a specification for defining environment agnostic configuration for cloud based workloads. 23 | This tool produces a Helm chart from the SCORE specification. 24 | Complete documentation is available at https://score.dev.`, 25 | Version: fmt.Sprintf("%s (build: %s; sha: %s)", version.Version, version.BuildTime, version.GitSHA), 26 | SilenceErrors: true, 27 | } 28 | ) 29 | 30 | func init() { 31 | rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s" .Version}} 32 | `) 33 | } 34 | 35 | func Execute() error { 36 | return rootCmd.Execute() 37 | } 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Golang image to create a build artifact. 2 | # This is based on Debian and sets the GOPATH to /go. 3 | # https://hub.docker.com/_/golang 4 | FROM golang:1.19 as builder 5 | 6 | # https://stackoverflow.com/questions/36279253/go-compiled-binary-wont-run-in-an-alpine-docker-container-on-ubuntu-host 7 | ENV CGO_ENABLED=0 8 | 9 | # Set the current working directory inside the container. 10 | WORKDIR /go/src/github.com/score-spec/score-helm 11 | 12 | # Copy the entire project and build it. 13 | COPY . . 14 | RUN GOOS=linux GOARCH=amd64 go build -o /usr/local/bin/score-helm ./cmd/score-helm 15 | 16 | # Use the official Alpine image for a lean production container. 17 | # https://hub.docker.com/_/alpine 18 | # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds 19 | FROM alpine:3 20 | 21 | # Set the current working directory inside the container. 22 | WORKDIR /score-helm 23 | 24 | # Copy the binary from the builder image. 25 | COPY --from=builder /usr/local/bin/score-helm /usr/local/bin/score-helm 26 | 27 | # Run the binary. 28 | ENTRYPOINT ["/usr/local/bin/score-helm"] 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### Description 4 | 5 | 6 | #### What does this PR do? 7 | 8 | 9 | 10 | 11 | 12 | #### Types of changes 13 | 14 | - [ ] Bug fix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 17 | - [ ] New chore (expected functionality to be implemented) 18 | 19 | #### Checklist: 20 | 21 | 22 | - [ ] My change requires a change to the documentation. 23 | - [ ] I have updated the documentation accordingly. 24 | - [ ] I've signed off with an email address that matches the commit author. 25 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # Check the documentation at https://goreleaser.com 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - id: score-helm 7 | binary: score-helm 8 | main: ./cmd/score-helm 9 | ldflags: 10 | - -X github.com/score-spec/score-helm/internal/version.Version={{ .Version }} 11 | - -X github.com/score-spec/score-helm/internal/version.BuildTime={{ .CommitDate }} 12 | - -X github.com/score-spec/score-helm/internal/version.GitSHA={{ .FullCommit }} 13 | env: 14 | - CGO_ENABLED=0 15 | targets: 16 | - linux_amd64_v1 17 | - linux_arm64 18 | - windows_amd64_v1 19 | - darwin_amd64_v1 20 | - darwin_arm64 21 | archives: 22 | - format_overrides: 23 | - goos: windows 24 | format: zip 25 | checksum: 26 | name_template: 'checksums.txt' 27 | snapshot: 28 | name_template: "{{ incpatch .Version }}-next" 29 | changelog: 30 | sort: asc 31 | filters: 32 | exclude: 33 | - '^docs:' 34 | - '^test:' 35 | brews: 36 | - name: score-helm 37 | homepage: "https://score.dev" 38 | tap: 39 | owner: score-spec 40 | name: homebrew-tap 41 | token: "{{ .Env.TAP_GITHUB_TOKEN }}" 42 | commit_author: 43 | name: rachfop 44 | email: prachford@icloud.com 45 | -------------------------------------------------------------------------------- /examples/chart/values.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Default values for the chart (for reference only). 3 | # An actual values file is rendered from the source SCORE file by the CLI tool. 4 | # 5 | 6 | # service: 7 | # type: ClusterIP 8 | # ports: 9 | # - name: www 10 | # protocol: TCP 11 | # port: 80 12 | # targetPort: 8080 13 | 14 | # containers: 15 | # my-container: 16 | # image: 17 | # name: busybox:latest 18 | # command: ["/bin/echo"] 19 | # args: 20 | # - "-c" 21 | # - "Hello $(FRIEND)" 22 | # env: 23 | # - name: "FRIEND" 24 | # value: "World!" 25 | # files: 26 | # - target: etc/hello-world/config.yaml 27 | # mode: "666" 28 | # content: ${resources.env.APP_CONFIG} 29 | # volumeMounts: 30 | # - name: ${resources.data} 31 | # subPath: sub/path 32 | # mountPath: /mnt/data 33 | # readOnly: true 34 | # livenessProbe: 35 | # httpGet: 36 | # path: /health 37 | # port: http 38 | # readinessProbe: 39 | # httpGet: 40 | # path: /ready 41 | # port: http 42 | # httpHeaders: 43 | # - name: Custom-Header 44 | # value: Awesome 45 | # resources: 46 | # limits: 47 | # cpu: 100m 48 | # memory: 128Mi 49 | # requests: 50 | # cpu: 100m 51 | # memory: 128Mi 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: [push] 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - 19 | name: Fetch all tags 20 | run: git fetch --force --tags 21 | - 22 | name: Set up Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version-file: 'go.mod' 26 | - 27 | name: Run Go Tests 28 | run: go test ./... -cover -race 29 | 30 | release: 31 | if: startsWith(github.ref, 'refs/tags/') 32 | needs: 33 | - test 34 | runs-on: ubuntu-latest 35 | steps: 36 | - 37 | name: Checkout 38 | uses: actions/checkout@v3 39 | with: 40 | fetch-depth: 0 41 | - 42 | name: Fetch all tags 43 | run: git fetch --force --tags 44 | - 45 | name: Set up Go 46 | uses: actions/setup-go@v3 47 | with: 48 | go-version-file: 'go.mod' 49 | - 50 | name: Run GoReleaser 51 | uses: goreleaser/goreleaser-action@v3 52 | with: 53 | distribution: goreleaser 54 | version: latest 55 | args: release --rm-dist 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /examples/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define ".name" -}} 5 | {{- end }} 6 | {{/* 7 | Create a default fully qualified app name. 8 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 9 | If release name contains chart name it will be used as a full name. 10 | */}} 11 | {{- define ".fullname" -}} 12 | {{- $name := default .Chart.Name .Values.nameOverride }} 13 | {{- if contains $name .Release.Name }} 14 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 15 | {{- else }} 16 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 17 | {{- end }} 18 | {{- end }} 19 | {{/* 20 | Create chart name and version as used by the chart label. 21 | */}} 22 | {{- define ".chart" -}} 23 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 24 | {{- end }} 25 | {{/* 26 | Common labels 27 | */}} 28 | {{- define ".labels" -}} 29 | helm.sh/chart: {{ include ".chart" . }} 30 | {{ include ".selectorLabels" . }} 31 | {{- if .Chart.AppVersion }} 32 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 33 | {{- end }} 34 | app.kubernetes.io/managed-by: {{ .Release.Service }} 35 | {{- end }} 36 | {{/* 37 | Selector labels 38 | */}} 39 | {{- define ".selectorLabels" -}} 40 | app.kubernetes.io/name: {{ include ".name" . }} 41 | app.kubernetes.io/instance: {{ .Release.Name }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include ".fullname" . }} 5 | labels: 6 | {{- include ".labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | matchLabels: 10 | {{- include ".selectorLabels" . | nindent 6 }} 11 | template: 12 | metadata: 13 | labels: 14 | {{- include ".selectorLabels" . | nindent 8 }} 15 | spec: 16 | containers: 17 | {{- range $name, $container := .Values.containers }} 18 | - name: {{ $name }} 19 | image: "{{ $container.image.name }}" 20 | {{- with $container.command }} 21 | command: 22 | {{- toYaml $container.command | nindent 12 }} 23 | {{- end }} 24 | {{- with $container.args }} 25 | args: 26 | {{- toYaml $container.args | nindent 12 }} 27 | {{- end }} 28 | {{- with $container.env }} 29 | env: 30 | {{- toYaml $container.env | nindent 12 }} 31 | {{- end }} 32 | {{- with $container.volumeMounts }} 33 | volumeMounts: 34 | {{- toYaml $container.volumeMounts | nindent 12 }} 35 | {{- end }} 36 | {{- with $container.livenessProbe }} 37 | livenessProbe: 38 | {{- toYaml $container.livenessProbe | nindent 12 }} 39 | {{- end }} 40 | {{- with $container.readinessProbe }} 41 | readinessProbe: 42 | {{- toYaml $container.readinessProbe | nindent 12 }} 43 | {{- end }} 44 | {{- with $container.resources }} 45 | resources: 46 | {{- toYaml $container.resources | nindent 12 }} 47 | {{- end }} 48 | {{- end }} 49 | -------------------------------------------------------------------------------- /examples/01-hello/README.md: -------------------------------------------------------------------------------- 1 | # 01 - Hello World! 2 | 3 | In this basic example there is a simple workload based on `busybox` Docker image described in a `score.yaml` file: 4 | 5 | ```yaml 6 | apiVersion: score.dev/v1b1 7 | 8 | metadata: 9 | name: hello-world 10 | 11 | containers: 12 | hello: 13 | image: busybox 14 | command: ["/bin/sh"] 15 | args: ["-c", "while true; do echo Hello World!; sleep 5; done"] 16 | ``` 17 | 18 | A generic chart template from `/examples/chart` can be used to prepare a Helm chart for this workload: 19 | 20 | ```console 21 | $ helm create -p ../examples/chart hello 22 | ``` 23 | 24 | Now the source `score.yaml` file should be converted into Helm values file with `score-helm` CLI tool: 25 | 26 | ```console 27 | $ score-helm run -f ./score.yaml -o ./values.yaml 28 | ``` 29 | 30 | Output `values.yaml` file would contain a workload configuration for the chart: 31 | 32 | ```yaml 33 | containers: 34 | hello: 35 | args: 36 | - -c 37 | - while true; do echo Hello World!; sleep 5; done 38 | command: 39 | - /bin/sh 40 | image: 41 | name: busybox 42 | ``` 43 | 44 | Deploying the Helm chart: 45 | 46 | ```console 47 | $ helm install --values ./values.yaml hello ./hello 48 | 49 | NAME: hello 50 | LAST DEPLOYED: Mon Oct 31 16:06:34 2022 51 | NAMESPACE: default 52 | STATUS: deployed 53 | REVISION: 1 54 | TEST SUITE: None 55 | ``` 56 | 57 | Resulting Kubernetes deployment object: 58 | 59 | ```yaml 60 | # Source: hello/templates/deployment.yaml 61 | apiVersion: apps/v1 62 | kind: Deployment 63 | metadata: 64 | name: hello 65 | labels: 66 | helm.sh/chart: hello-0.1.0 67 | app.kubernetes.io/name: 68 | app.kubernetes.io/instance: hello 69 | app.kubernetes.io/version: "0.1.0" 70 | app.kubernetes.io/managed-by: Helm 71 | spec: 72 | selector: 73 | matchLabels: 74 | app.kubernetes.io/name: 75 | app.kubernetes.io/instance: hello 76 | template: 77 | metadata: 78 | labels: 79 | app.kubernetes.io/name: 80 | app.kubernetes.io/instance: hello 81 | spec: 82 | containers: 83 | - name: hello 84 | image: "busybox" 85 | command: 86 | - /bin/sh 87 | args: 88 | - -c 89 | - while true; do echo Hello World!; sleep 5; done 90 | ``` 91 | -------------------------------------------------------------------------------- /examples/02-environment/README.md: -------------------------------------------------------------------------------- 1 | # 02 - Environment Variables 2 | 3 | It is common to pass a dynamic environment-specific configuration to the container during the deployment. 4 | 5 | In score specification a special `environment` resource type is used to support such cases: 6 | 7 | ```yaml 8 | apiVersion: score.dev/v1b1 9 | 10 | metadata: 11 | name: hello-world 12 | 13 | containers: 14 | hello: 15 | image: busybox 16 | command: ["/bin/sh"] 17 | args: ["-c", "while true; do echo Hello $${FRIEND}!; sleep 5; done"] 18 | variables: 19 | FRIEND: ${resources.env.NAME} 20 | 21 | resources: 22 | env: 23 | type: environment 24 | ``` 25 | 26 | If there is a need to set the `NAME` variable value for the environment the workload would be deployed into, an `env.yaml` file can be used: 27 | 28 | ```yaml 29 | env: 30 | NAME: John 31 | ``` 32 | 33 | Now the source `score.yaml` file and the `env.yaml` file should be combined and converted into Helm values file with `score-helm` CLI tool: 34 | 35 | ```console 36 | $ score-helm run -f ./score.yaml --values ./env.yaml -o ./values.yaml 37 | ``` 38 | 39 | Output `values.yaml` file would contain a workload configuration for the chart: 40 | 41 | ```yaml 42 | containers: 43 | hello: 44 | args: 45 | - -c 46 | - while true; do echo Hello $${FRIEND}!; sleep 5; done 47 | command: 48 | - /bin/sh 49 | env: 50 | - name: FRIEND 51 | value: John 52 | image: 53 | name: busybox 54 | ``` 55 | 56 | A generic chart template from `/examples/chart` can be used to prepare a Helm chart for this example: 57 | 58 | ```console 59 | $ helm create -p ../examples/chart hello 60 | ``` 61 | 62 | Deploying the Helm chart: 63 | 64 | ```console 65 | $ helm install --values ./values.yaml hello ./hello 66 | 67 | NAME: hello 68 | LAST DEPLOYED: Wed Nov 2 14:40:11 2022 69 | NAMESPACE: default 70 | STATUS: deployed 71 | REVISION: 1 72 | TEST SUITE: None 73 | ``` 74 | 75 | Resulting Kubernetes deployment object: 76 | 77 | ```yaml 78 | # Source: hello/templates/deployment.yaml 79 | apiVersion: apps/v1 80 | kind: Deployment 81 | metadata: 82 | name: hello 83 | labels: 84 | helm.sh/chart: hello-0.1.0 85 | app.kubernetes.io/name: 86 | app.kubernetes.io/instance: hello 87 | app.kubernetes.io/version: "0.1.0" 88 | app.kubernetes.io/managed-by: Helm 89 | spec: 90 | selector: 91 | matchLabels: 92 | app.kubernetes.io/name: 93 | app.kubernetes.io/instance: hello 94 | template: 95 | metadata: 96 | labels: 97 | app.kubernetes.io/name: 98 | app.kubernetes.io/instance: hello 99 | spec: 100 | containers: 101 | - name: hello 102 | image: "busybox" 103 | command: 104 | - /bin/sh 105 | args: 106 | - -c 107 | - while true; do echo Hello $${FRIEND}!; sleep 5; done 108 | env: 109 | - name: FRIEND 110 | value: John 111 | ``` 112 | -------------------------------------------------------------------------------- /internal/helm/templates.go: -------------------------------------------------------------------------------- 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 helm 9 | 10 | import ( 11 | "fmt" 12 | "log" 13 | "regexp" 14 | "strings" 15 | 16 | "github.com/mitchellh/mapstructure" 17 | 18 | score "github.com/score-spec/score-go/types" 19 | ) 20 | 21 | var ( 22 | placeholderRegEx = regexp.MustCompile(`\$(\$|{([a-zA-Z0-9.\-_\[\]"'#]+)})`) 23 | ) 24 | 25 | // templatesContext ia an utility type that provides a context for '${...}' templates substitution 26 | type templatesContext struct { 27 | meta map[string]interface{} 28 | resources score.WorkloadResources 29 | values map[string]interface{} 30 | } 31 | 32 | // buildContext initializes a new templatesContext instance 33 | func buildContext(metadata score.WorkloadMetadata, resources score.WorkloadResources, values map[string]interface{}) (*templatesContext, error) { 34 | var metadataMap = make(map[string]interface{}) 35 | if decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 36 | TagName: "json", 37 | Result: &metadataMap, 38 | }); err != nil { 39 | return nil, err 40 | } else { 41 | decoder.Decode(metadata) 42 | } 43 | 44 | return &templatesContext{ 45 | meta: metadataMap, 46 | resources: resources, 47 | values: values, 48 | }, nil 49 | } 50 | 51 | // Substitute replaces all matching '${...}' templates in a source string 52 | func (ctx *templatesContext) Substitute(src string) string { 53 | return placeholderRegEx.ReplaceAllStringFunc(src, func(str string) string { 54 | // WORKAROUND: ReplaceAllStringFunc(..) does not provide match details 55 | // https://github.com/golang/go/issues/5690 56 | var matches = placeholderRegEx.FindStringSubmatch(str) 57 | 58 | // SANITY CHECK 59 | if len(matches) != 3 { 60 | log.Printf("Error: could not find a proper match in previously captured string fragment") 61 | return src 62 | } 63 | 64 | // EDGE CASE: Captures "$$" sequences and empty templates "${}" 65 | if matches[2] == "" { 66 | return matches[1] 67 | } 68 | 69 | return ctx.mapVar(matches[2]) 70 | }) 71 | } 72 | 73 | // MapVar replaces objects and properties references with corresponding values 74 | // Returns an empty string if the reference can't be resolved 75 | func (ctx *templatesContext) mapVar(ref string) string { 76 | if ref == "" || ref == "$" { 77 | return ref 78 | } 79 | 80 | var segments = strings.SplitN(ref, ".", 2) 81 | switch segments[0] { 82 | case "metadata": 83 | if len(segments) == 2 { 84 | if val, exists := ctx.meta[segments[1]]; exists { 85 | return fmt.Sprintf("%v", val) 86 | } 87 | } 88 | 89 | case "resources": 90 | if len(segments) == 2 { 91 | segments = strings.SplitN(segments[1], ".", 2) 92 | var resName = segments[0] 93 | if _, exists := ctx.resources[resName]; exists { 94 | if len(segments) == 1 { 95 | return resName 96 | } else { 97 | var propName = segments[1] 98 | 99 | var val = "" 100 | if src, ok := ctx.values[resName]; ok { 101 | if srcMap, ok := src.(map[string]interface{}); ok { 102 | if v, ok := srcMap[propName]; ok { 103 | val = fmt.Sprintf("%v", v) 104 | } 105 | } 106 | } 107 | 108 | return val 109 | } 110 | } 111 | } 112 | } 113 | 114 | log.Printf("Warning: Can not resolve '%s' reference.", ref) 115 | return "" 116 | } 117 | -------------------------------------------------------------------------------- /internal/helm/templates_test.go: -------------------------------------------------------------------------------- 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 helm 9 | 10 | import ( 11 | "testing" 12 | 13 | score "github.com/score-spec/score-go/types" 14 | assert "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestMapVar(t *testing.T) { 18 | var meta = score.WorkloadMetadata{ 19 | "name": "test-name", 20 | } 21 | 22 | var resources = score.WorkloadResources{ 23 | "env": score.Resource{ 24 | Type: "environment", 25 | }, 26 | "db": score.Resource{ 27 | Type: "postgres", 28 | }, 29 | "dns": score.Resource{ 30 | Type: "dns", 31 | }, 32 | } 33 | 34 | var values = map[string]interface{}{ 35 | "db": map[string]interface{}{ 36 | "host": "localhost", 37 | "name": "test-db", 38 | }, 39 | "dns": map[string]interface{}{ 40 | "domain": "test.domain.name", 41 | }, 42 | } 43 | 44 | context, err := buildContext(meta, resources, values) 45 | assert.NoError(t, err) 46 | 47 | assert.Equal(t, "", context.mapVar("")) 48 | assert.Equal(t, "$", context.mapVar("$")) 49 | 50 | assert.Equal(t, "test-name", context.mapVar("metadata.name")) 51 | assert.Equal(t, "", context.mapVar("metadata.name.nil")) 52 | assert.Equal(t, "", context.mapVar("metadata.nil")) 53 | 54 | assert.Equal(t, "", context.mapVar("resources.env.DEBUG")) 55 | 56 | assert.Equal(t, "db", context.mapVar("resources.db")) 57 | assert.Equal(t, "localhost", context.mapVar("resources.db.host")) 58 | assert.Equal(t, "", context.mapVar("resources.db.port")) 59 | assert.Equal(t, "test-db", context.mapVar("resources.db.name")) 60 | assert.Equal(t, "", context.mapVar("resources.db.name.nil")) 61 | assert.Equal(t, "", context.mapVar("resources.db.nil")) 62 | assert.Equal(t, "", context.mapVar("resources.nil")) 63 | assert.Equal(t, "", context.mapVar("nil.db.name")) 64 | } 65 | 66 | func TestSubstitute(t *testing.T) { 67 | var meta = score.WorkloadMetadata{ 68 | "name": "test-name", 69 | } 70 | 71 | var resources = score.WorkloadResources{ 72 | "env": score.Resource{ 73 | Type: "environment", 74 | }, 75 | "db": score.Resource{ 76 | Type: "postgres", 77 | }, 78 | "dns": score.Resource{ 79 | Type: "dns", 80 | }, 81 | } 82 | 83 | var values = map[string]interface{}{ 84 | "db": map[string]interface{}{ 85 | "host": "localhost", 86 | "name": "test-db", 87 | }, 88 | "dns": map[string]interface{}{ 89 | "domain": "test.domain.name", 90 | }, 91 | } 92 | 93 | context, err := buildContext(meta, resources, values) 94 | assert.NoError(t, err) 95 | 96 | assert.Equal(t, "", context.Substitute("")) 97 | assert.Equal(t, "abc", context.Substitute("abc")) 98 | assert.Equal(t, "$abc", context.Substitute("$abc")) 99 | assert.Equal(t, "abc $ abc", context.Substitute("abc $$ abc")) 100 | assert.Equal(t, "${abc}", context.Substitute("$${abc}")) 101 | 102 | assert.Equal(t, "The name is 'test-name'", context.Substitute("The name is '${metadata.name}'")) 103 | assert.Equal(t, "The name is ''", context.Substitute("The name is '${metadata.nil}'")) 104 | 105 | assert.Equal(t, "resources.badref.DEBUG", context.Substitute("resources.badref.DEBUG")) 106 | 107 | assert.Equal(t, "db", context.Substitute("${resources.db}")) 108 | assert.Equal(t, 109 | "postgresql://:@localhost:/test-db", 110 | context.Substitute("postgresql://${resources.db.user}:${resources.db.password}@${resources.db.host}:${resources.db.port}/${resources.db.name}")) 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | :warning: Deprecation Notice :warning: 5 | 6 | We have deprecated the score-helm CLI implementation. To get started with Score, we recommend using one of our reference implementations [score-compose](https://github.com/score-spec/score-compose) or [score-k8s](https://github.com/score-spec/score-k8s). If you're interested in developing a score-helm reference implementation, we'd love to support you! Please reach out to us for assistance and collaboration. 7 | 8 | --- 9 | 10 | ![Score banner](docs/images/banner.png) 11 | 12 | # ![Score](docs/images/logo.svg) Score overview 13 | 14 | Score aims to improve developer producticity and experience by reducing the risk of configuration inconsistencies between local and remote environments. It provides developer-centric workload specification (`score.yaml`) which captures a workloads runtime requirements in a platform-agnostic manner. Learn more [here](https://github.com/score-spec/spec#-what-is-score). 15 | 16 | The `score.yaml` specification file can be executed against a _Score Implementation CLI_, a conversion tool for application developers to generate environment specific configuration. In combination with environment specific parameters, the CLI tool can run your workload in the target environment by generating a platform-specific configuration file. The `score-helm` CLI is a reference implementation used to generate `values.yaml` files for your Helm chart. 17 | 18 | ## ![Installation](docs/images/install.svg) Installation 19 | 20 | To install `score-helm`, follow the instructions as described in our [installation guide](https://docs.score.dev/docs/score-implementation/other/score-helm/#installation). 21 | 22 | ## ![Get Started](docs/images/overview.svg) Get Started 23 | 24 | If you already have a `score.yaml` file defined, you can simply run the following command: 25 | 26 | ```bash 27 | # Prepare a new Helm values file 28 | score-helm run -f ./score.yaml -o ./values.yaml 29 | ``` 30 | 31 | - `run` tells the CLI to translate the Score file to a Helm `values.yaml` file. 32 | - `-f` is the path to the Score file. 33 | - `--env` specifies the path to the `values.yaml` file. 34 | 35 | If you're just getting started, follow [this guide](https://docs.score.dev/docs/get-started/score-helm-hello-world/) to run your first Hello World program with `score-helm`. 36 | 37 | ## ![Get involved](docs/images/get-involved.svg) Get involved 38 | 39 | - Give the project a star! 40 | - Contact us via email: 41 | - team@score.dev 42 | - abuse@score.dev 43 | - See our [documentation](https://docs.score.dev) 44 | 45 | ## ![Contributing](docs/images/contributing.svg) Contributing 46 | 47 | - Write a [blog post](https://score.dev/blog) 48 | - Provide feedback on our [roadmap](https://github.com/score-spec/spec/blob/main/roadmap.md#get-involved) 49 | - Contribute 50 | 51 | Contributions are what make the open-source community such a wonderful place to learn, inspire, and create. Any contributions you make are greatly appreciated. 52 | 53 | If you have a suggestion that would improve this, please fork the repo and create a pull request. You can also open an issue with the tag `enhancement`. 54 | 55 | 1. Fork the Project. 56 | 2. Create your Feature Branch. `git checkout -b feature/feature-name` 57 | 3. Commit your Changes. `git commit -s -m "Add some AmazingFeature"` 58 | 4. Push to the Branch. `git push origin feature/feature-name` 59 | 5. Open a Pull Request. 60 | 61 | Read [CONTRIBUTING](CONTRIBUTING.md) for more information. 62 | 63 | ### Documentation 64 | 65 | You can find our documentation at [docs.score.dev](https://docs.score.dev/docs). 66 | 67 | ### Roadmap 68 | 69 | See [Roadmap](https://github.com/score-spec/spec/blob/main/roadmap.md). You can [submit an idea](https://github.com/score-spec/spec/blob/main/roadmap.md#get-involved) anytime. 70 | 71 | ### License 72 | 73 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 74 | 75 | ### Code of conduct 76 | 77 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) 78 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 6 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 7 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 8 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 15 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 19 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= 20 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= 21 | github.com/score-spec/score-go v1.1.0 h1:63WM1u93NtGgMuPtVZ/UBfzg/BpYuY8sBquaL0BkrXU= 22 | github.com/score-spec/score-go v1.1.0/go.mod h1:nt6TOq2Ld9SiH3Fd9NF8tiJ9L7S17OE3FNgCrSet5GQ= 23 | github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= 24 | github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 25 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 26 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 29 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 31 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 32 | github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= 33 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 34 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 35 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 36 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 37 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 38 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 39 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 42 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 46 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 47 | -------------------------------------------------------------------------------- /internal/helm/write_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package helm 9 | 10 | import ( 11 | "bufio" 12 | "bytes" 13 | "testing" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestYamlEncode(t *testing.T) { 19 | var tests = []struct { 20 | Name string 21 | Source map[string]interface{} 22 | Output []byte 23 | Error error 24 | }{ 25 | { 26 | Name: "Should encode the values", 27 | Source: map[string]interface{}{ 28 | "service": map[string]interface{}{ 29 | "type": "ClusterIP", 30 | "ports": []interface{}{ 31 | map[string]interface{}{ 32 | "name": "www", 33 | "protocol": "TCP", 34 | "port": 80, 35 | "targetPort": 8080, 36 | }, 37 | }, 38 | }, 39 | "containers": map[string]interface{}{ 40 | "my-container": map[string]interface{}{ 41 | "image": map[string]interface{}{ 42 | "name": "busybox:latest", 43 | }, 44 | "command": []string{"/bin/echo"}, 45 | "args": []string{ 46 | "-c", 47 | "Hello $(FRIEND)", 48 | }, 49 | "env": []interface{}{ 50 | map[string]interface{}{ 51 | "name": "FRIEND", 52 | "value": "World!", 53 | }, 54 | }, 55 | "volumeMounts": []interface{}{ 56 | map[string]interface{}{ 57 | "name": "${resources.data}", 58 | "subPath": "sub/path", 59 | "mountPath": "/mnt/data", 60 | "readOnly": true, 61 | }, 62 | }, 63 | "livenessProbe": map[string]interface{}{ 64 | "httpGet": map[string]interface{}{ 65 | "path": "/health", 66 | "port": "http", 67 | }, 68 | }, 69 | "readinessProbe": map[string]interface{}{ 70 | "httpGet": map[string]interface{}{ 71 | "path": "/ready", 72 | "port": "http", 73 | "httpHeaders": []interface{}{ 74 | map[string]interface{}{ 75 | "name": "Custom-Header", 76 | "value": "Awesome", 77 | }, 78 | }, 79 | }, 80 | }, 81 | "resources": map[string]interface{}{ 82 | "limits": map[string]interface{}{ 83 | "cpu": "100m", 84 | "memory": "128Mi", 85 | }, 86 | "requests": map[string]interface{}{ 87 | "cpu": "100m", 88 | "memory": "128Mi", 89 | }, 90 | }, 91 | }, 92 | }, 93 | }, 94 | Output: []byte(`containers: 95 | my-container: 96 | args: 97 | - -c 98 | - Hello $(FRIEND) 99 | command: 100 | - /bin/echo 101 | env: 102 | - name: FRIEND 103 | value: World! 104 | image: 105 | name: busybox:latest 106 | livenessProbe: 107 | httpGet: 108 | path: /health 109 | port: http 110 | readinessProbe: 111 | httpGet: 112 | httpHeaders: 113 | - name: Custom-Header 114 | value: Awesome 115 | path: /ready 116 | port: http 117 | resources: 118 | limits: 119 | cpu: 100m 120 | memory: 128Mi 121 | requests: 122 | cpu: 100m 123 | memory: 128Mi 124 | volumeMounts: 125 | - mountPath: /mnt/data 126 | name: ${resources.data} 127 | readOnly: true 128 | subPath: sub/path 129 | service: 130 | ports: 131 | - name: www 132 | port: 80 133 | protocol: TCP 134 | targetPort: 8080 135 | type: ClusterIP 136 | `), 137 | }, 138 | } 139 | 140 | for _, tt := range tests { 141 | t.Run(tt.Name, func(t *testing.T) { 142 | buf := bytes.Buffer{} 143 | w := bufio.NewWriter(&buf) 144 | 145 | err := WriteYAML(w, tt.Source) 146 | w.Flush() 147 | 148 | if tt.Error != nil { 149 | // On Error 150 | // 151 | assert.ErrorContains(t, err, tt.Error.Error()) 152 | } else { 153 | // On Success 154 | // 155 | assert.NoError(t, err) 156 | assert.Equal(t, tt.Output, buf.Bytes()) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /internal/helm/convert.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package helm 9 | 10 | import ( 11 | "fmt" 12 | "sort" 13 | 14 | score "github.com/score-spec/score-go/types" 15 | ) 16 | 17 | // getProbeDetails extracts an httpGet probe details from the source spec. 18 | // Returns nil if the source spec is empty. 19 | func getProbeDetails(probe *score.ContainerProbe) map[string]interface{} { 20 | if probe.HttpGet.Path == "" { 21 | return nil 22 | } 23 | 24 | var res = map[string]interface{}{ 25 | "type": "http", 26 | "path": probe.HttpGet.Path, 27 | "port": probe.HttpGet.Port, 28 | } 29 | 30 | if len(probe.HttpGet.HttpHeaders) > 0 { 31 | var hdrs = map[string]string{} 32 | for _, hdr := range probe.HttpGet.HttpHeaders { 33 | if hdr.Name != nil && hdr.Value != nil { 34 | hdrs[*hdr.Name] = *hdr.Value 35 | } 36 | } 37 | res["httpHeaders"] = hdrs 38 | } 39 | 40 | return res 41 | } 42 | 43 | // ConvertSpec converts SCORE specification into Helm values map. 44 | func ConvertSpec(dest map[string]interface{}, spec *score.Workload, values map[string]interface{}) error { 45 | if values == nil { 46 | values = make(map[string]interface{}) 47 | } 48 | context, err := buildContext(spec.Metadata, spec.Resources, values) 49 | if err != nil { 50 | return fmt.Errorf("preparing context: %w", err) 51 | } 52 | 53 | if spec.Service != nil && len(spec.Service.Ports) > 0 { 54 | var ports = make([]interface{}, 0, len(spec.Service.Ports)) 55 | for name, port := range spec.Service.Ports { 56 | var pVals = map[string]interface{}{ 57 | "name": name, 58 | "port": port.Port, 59 | } 60 | if port.Protocol != nil { 61 | pVals["protocol"] = string(*port.Protocol) 62 | } 63 | if port.TargetPort != nil { 64 | pVals["targetPort"] = *port.TargetPort 65 | } 66 | ports = append(ports, pVals) 67 | } 68 | 69 | // NOTE: Sorting is necessary for DeepEqual call within our Unit Tests to work reliably 70 | sort.Slice(ports, func(i, j int) bool { 71 | return ports[i].(map[string]interface{})["name"].(string) < ports[j].(map[string]interface{})["name"].(string) 72 | }) 73 | // END (NOTE) 74 | dest["service"] = map[string]interface{}{ 75 | "type": "ClusterIP", 76 | "ports": ports, 77 | } 78 | } 79 | 80 | var containers = map[string]interface{}{} 81 | for name, cSpec := range spec.Containers { 82 | var cVals = map[string]interface{}{ 83 | "image": map[string]interface{}{ 84 | "name": cSpec.Image, 85 | }, 86 | } 87 | 88 | if len(cSpec.Command) > 0 { 89 | cVals["command"] = cSpec.Command 90 | } 91 | if len(cSpec.Args) > 0 { 92 | cVals["args"] = cSpec.Args 93 | } 94 | if len(cSpec.Variables) > 0 { 95 | var env = make([]interface{}, 0, len(cSpec.Variables)) 96 | for key, val := range cSpec.Variables { 97 | val = context.Substitute(val) 98 | env = append(env, map[string]interface{}{"name": key, "value": val}) 99 | } 100 | 101 | // NOTE: Sorting is necessary for DeepEqual call within our Unit Tests to work reliably 102 | sort.Slice(env, func(i, j int) bool { 103 | return env[i].(map[string]interface{})["name"].(string) < env[j].(map[string]interface{})["name"].(string) 104 | }) 105 | // END (NOTE) 106 | cVals["env"] = env 107 | } 108 | 109 | if len(cSpec.Volumes) > 0 { 110 | var volumes = make([]interface{}, 0, len(cSpec.Volumes)) 111 | for _, vol := range cSpec.Volumes { 112 | var source = context.Substitute(vol.Source) 113 | var vVals = map[string]interface{}{ 114 | "name": source, 115 | "mountPath": vol.Target, 116 | } 117 | if vol.Path != nil { 118 | vVals["subPath"] = *vol.Path 119 | } 120 | if vol.ReadOnly != nil { 121 | vVals["readOnly"] = *vol.ReadOnly 122 | } 123 | volumes = append(volumes, vVals) 124 | } 125 | cVals["volumeMounts"] = volumes 126 | } 127 | 128 | if cSpec.LivenessProbe != nil { 129 | if probe := getProbeDetails(cSpec.LivenessProbe); len(probe) > 0 { 130 | cVals["livenessProbe"] = probe 131 | } 132 | } 133 | if cSpec.ReadinessProbe != nil { 134 | if probe := getProbeDetails(cSpec.ReadinessProbe); len(probe) > 0 { 135 | cVals["readinessProbe"] = probe 136 | } 137 | } 138 | 139 | if cSpec.Resources != nil { 140 | containerResources := make(map[string]interface{}) 141 | if out := getContainerResources(cSpec.Resources.Limits); len(out) > 0 { 142 | containerResources["limits"] = out 143 | } 144 | if out := getContainerResources(cSpec.Resources.Requests); len(out) > 0 { 145 | containerResources["requests"] = out 146 | } 147 | if len(containerResources) > 0 { 148 | cVals["resources"] = containerResources 149 | } 150 | } 151 | containers[name] = cVals 152 | } 153 | dest["containers"] = containers 154 | 155 | return nil 156 | } 157 | 158 | func getContainerResources(requests *score.ResourcesLimits) map[string]interface{} { 159 | out := make(map[string]interface{}) 160 | if requests.Cpu != nil { 161 | out["cpu"] = *requests.Cpu 162 | } 163 | if requests.Memory != nil { 164 | out["memory"] = *requests.Memory 165 | } 166 | return out 167 | } 168 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor guidelines 2 | 3 | We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. 4 | 5 | When contributing to this project, you must agree that you have authored the content, that you have the necessary rights to the content and that the content you contribute may be provided under the [APACHE LICENSE, VERSION 2.0](LICENSE). 6 | 7 | ### **What do I need to know to help?** 8 | 9 | If you are looking to help to with a code contribution, our project uses Go Lang. If you don't feel ready to make a code contribution yet, no problem! You can also check out the documentation issues https://github.com/score-spec/score-helm/issues. 10 | 11 | If you are interested in making a code contribution and would like to learn more about the technologies that we use, check out the list below. 12 | 13 | - [https://humanitec.com](https://humanitec.com/) 14 | - [https://docs.docker.com/compose/](https://docs.docker.com/compose/) 15 | - [https://helm.sh/docs/](https://helm.sh/docs/) 16 | 17 | ### **How do I make a contribution?** 18 | 19 | Never made an open-source contribution before? Wondering how contributions work in our project? Here's a quick rundown! 20 | 21 | 1. Find an issue that you are interested in addressing or a feature that you would like to add. 22 | 2. Fork the repository associated with the issue to your local GitHub organization. This means that you will have a copy of the repository under **your-GitHub-username/repository-name**. 23 | 3. Clone the repository to your local machine using **git clone**. 24 | 4. Create a new branch for your fix using **git checkout -b your-branch-name**. 25 | 5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add. 26 | 6. Use **git add insert-paths-of-changed-files-here** to add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index. 27 | 7. Use **git commit -s -m "Insert a brief message of the changes made here"** to store the contents of the index with a descriptive message. 28 | 8. Push the changes to the remote repository using **git push origin your-branch-name**. 29 | 9. Submit a pull request to the upstream repository. 30 | 10. Title the pull request with a brief description of the changes made and the issue or bug number associated with your change. For example, you can title an issue like so, "Added more log outputting to resolve #4352". 31 | 11. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you resolve any problems and improve it! 32 | 12. Wait for the pull request to be reviewed by a maintainer. 33 | 13. Introduce changes to the pull request if the reviewing maintainer recommends them. 34 | 14. Celebrate your success after your pull request is merged! 35 | 36 | ## Feature requests 37 | 38 | ## Code reviews 39 | 40 | All submissions, including submissions by project members, require review. 41 | 42 | Score uses GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about1.pull1.requests/) for more information on using pull requests. 43 | 44 | The general workflow for code contributions: 45 | 46 | 1. Submit/find an issue in this repository. 47 | 1. Clone the relevant repo. 48 | 1. Make your code change. 49 | 1. Write tests and update docs. 50 | 1. Build and test locally. 51 | 1. Submit a pull request. 52 | 1. Iterate as needed. 53 | 1. Your PR will be approved and merged. 54 | 55 | ### **Where can I go for help?** 56 | 57 | If you need help, you can create an issue. 58 | 59 | ### Reporting bugs 60 | 61 | #### Before submitting an issue 62 | 63 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 64 | 65 | - Make sure that you are using the latest version. 66 | - Determine if your bug is really a bug and not an error on your side for example using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/score-dev/docs). If you are looking for support. 67 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug. 68 | - Also make sure to search the internet (including Stack Overflow) to see if users outside the GitHub community have discussed the issue. 69 | - Collect information about the bug: 70 | - Stack trace (Traceback). 71 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM). 72 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 73 | - Possibly your input and the output. 74 | - Can you reliably reproduce the issue? 75 | 76 | ### **What does the Code of Conduct mean for me?** 77 | 78 | Our [Code of Conduct](CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy, regardless of their identity. If you are the victim of any inappropriate behavior or comments as described in our Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code. 79 | -------------------------------------------------------------------------------- /internal/helm/convert_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package helm 9 | 10 | import ( 11 | "testing" 12 | 13 | score "github.com/score-spec/score-go/types" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestScoreConvert(t *testing.T) { 18 | var tests = []struct { 19 | Name string 20 | Spec *score.Workload 21 | Values map[string]interface{} 22 | Expected map[string]interface{} 23 | Error error 24 | }{ 25 | // Success path 26 | // 27 | { 28 | Name: "Should convert SCORE to Helm values", 29 | Spec: &score.Workload{ 30 | Metadata: score.WorkloadMetadata{ 31 | "name": "test", 32 | }, 33 | Service: &score.WorkloadService{ 34 | Ports: score.WorkloadServicePorts{ 35 | "www": score.ServicePort{ 36 | Port: 80, 37 | TargetPort: Ref(8080), 38 | }, 39 | "admin": score.ServicePort{ 40 | Port: 8080, 41 | Protocol: Ref(score.ServicePortProtocolUDP), 42 | }, 43 | }, 44 | }, 45 | Containers: score.WorkloadContainers{ 46 | "backend": score.Container{ 47 | Image: "busybox", 48 | Command: []string{ 49 | "/bin/sh", 50 | }, 51 | Args: []string{ 52 | "-c", 53 | "while true; do printenv; echo ...sleeping 10 sec...; sleep 10; done", 54 | }, 55 | Variables: map[string]string{ 56 | "CONNECTION_STRING": "test connection string", 57 | }, 58 | }, 59 | }, 60 | }, 61 | Values: nil, 62 | Expected: map[string]interface{}{ 63 | "service": map[string]interface{}{ 64 | "type": "ClusterIP", 65 | "ports": []interface{}{ 66 | map[string]interface{}{ 67 | "name": "admin", 68 | "port": 8080, 69 | "protocol": "UDP", 70 | }, 71 | map[string]interface{}{ 72 | "name": "www", 73 | "port": 80, 74 | "targetPort": 8080, 75 | }, 76 | }, 77 | }, 78 | "containers": map[string]interface{}{ 79 | "backend": map[string]interface{}{ 80 | "image": map[string]interface{}{ 81 | "name": "busybox", 82 | }, 83 | "command": []string{"/bin/sh"}, 84 | "args": []string{ 85 | "-c", 86 | "while true; do printenv; echo ...sleeping 10 sec...; sleep 10; done", 87 | }, 88 | "env": []interface{}{ 89 | map[string]interface{}{ 90 | "name": "CONNECTION_STRING", 91 | "value": "test connection string", 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | { 99 | Name: "Should convert all resources references", 100 | Spec: &score.Workload{ 101 | Metadata: score.WorkloadMetadata{ 102 | "name": "test", 103 | }, 104 | Containers: score.WorkloadContainers{ 105 | "backend": score.Container{ 106 | Image: "busybox", 107 | Variables: map[string]string{ 108 | "DEBUG": "${resources.env.DEBUG}", 109 | "LOGS_LEVEL": "$${LOGS_LEVEL}", 110 | "DOMAIN_NAME": "${resources.dns.domain_name}", 111 | "CONNECTION_STRING": "postgresql://${resources.app-db.host}:${resources.app-db.port}/${resources.app-db.name}", 112 | }, 113 | Volumes: []score.ContainerVolumesElem{ 114 | { 115 | Source: "${resources.data}", 116 | Path: Ref("sub/path"), 117 | Target: "/mnt/data", 118 | ReadOnly: Ref(true), 119 | }, 120 | }, 121 | }, 122 | }, 123 | Resources: score.WorkloadResources{ 124 | "env": { 125 | Type: "environment", 126 | }, 127 | "app-db": { 128 | Type: "postgres", 129 | }, 130 | "dns": { 131 | Type: "dns", 132 | }, 133 | "data": { 134 | Type: "volume", 135 | }, 136 | }, 137 | }, 138 | Values: map[string]interface{}{ 139 | "app-db": map[string]interface{}{ 140 | "host": ".", 141 | "port": 5432, 142 | "name": "test-db", 143 | "user.name": "", 144 | "password": "", 145 | }, 146 | "dns": map[string]interface{}{ 147 | "domain": "test.domain.name", 148 | }, 149 | }, 150 | Expected: map[string]interface{}{ 151 | "containers": map[string]interface{}{ 152 | "backend": map[string]interface{}{ 153 | "image": map[string]interface{}{ 154 | "name": "busybox", 155 | }, 156 | "env": []interface{}{ 157 | map[string]interface{}{ 158 | "name": "CONNECTION_STRING", 159 | "value": "postgresql://.:5432/test-db", 160 | }, 161 | map[string]interface{}{ 162 | "name": "DEBUG", 163 | "value": "", // fallback to default value 164 | }, 165 | map[string]interface{}{ 166 | "name": "DOMAIN_NAME", 167 | "value": "", // referenced property does not exist 168 | }, 169 | map[string]interface{}{ 170 | "name": "LOGS_LEVEL", 171 | "value": "${LOGS_LEVEL}", // do not expand escaped sequences, e.g. "$${..}" 172 | }, 173 | }, 174 | "volumeMounts": []interface{}{ 175 | map[string]interface{}{ 176 | "name": "data", // expands to the resource name 177 | "subPath": "sub/path", 178 | "mountPath": "/mnt/data", 179 | "readOnly": true, 180 | }, 181 | }, 182 | }, 183 | }, 184 | }, 185 | }, 186 | } 187 | 188 | for _, tt := range tests { 189 | t.Run(tt.Name, func(t *testing.T) { 190 | var res = make(map[string]interface{}) 191 | err := ConvertSpec(res, tt.Spec, tt.Values) 192 | 193 | if tt.Error != nil { 194 | // On Error 195 | // 196 | assert.ErrorContains(t, err, tt.Error.Error()) 197 | } else { 198 | // On Success 199 | // 200 | assert.NoError(t, err) 201 | assert.Equal(t, tt.Expected, res) 202 | } 203 | }) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of gender identity and expression, sexual orientation, disability, physical appearance, body size, citizenship, nationality, ethnic or social origin, pregnancy, familial status, veteran status, genetic information, religion or belief (or lack thereof), membership of a national minority, property, age, education, socio-economic status, technical choices, and experience level. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, 8 | diverse, inclusive, and healthy community. 9 | 10 | We do not tolerate harassment by and/or of members of our community in any form. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to a positive environment for our 15 | community include: 16 | 17 | - Demonstrating empathy and kindness toward other people 18 | - Being respectful of differing opinions, viewpoints, and experiences 19 | - Giving and gracefully accepting constructive feedback 20 | - Accepting responsibility and apologizing to those affected by our mistakes, 21 | and learning from the experience 22 | - Focusing on what is best not just for us as individuals, but for the overall 23 | community 24 | - Giving everyone an opportunity to be heard 25 | - Using welcoming and inclusive language 26 | 27 | Examples of unacceptable behavior include: 28 | 29 | - The use of sexualized language or imagery, and sexual attention or advances of 30 | any kind 31 | - Trolling, insulting or derogatory remarks, and personal or political attacks 32 | - Public or private harassment 33 | - Publishing others' private information, such as a physical or email address, 34 | without their explicit permission 35 | - Other conduct which could reasonably be considered inappropriate in a 36 | professional setting 37 | 38 | ## Enforcement Responsibilities 39 | 40 | Community leaders are responsible for clarifying and enforcing our standards of 41 | acceptable behavior and will take appropriate and fair corrective action in 42 | response to any behavior that they deem inappropriate, threatening, offensive, 43 | or harmful. 44 | 45 | Community leaders have the right and responsibility to remove, edit, or reject 46 | comments, commits, code, wiki edits, issues, and other contributions that are 47 | not aligned to this Code of Conduct, and will communicate reasons for moderation 48 | decisions when appropriate. 49 | 50 | ## Scope 51 | 52 | This Code of Conduct applies within all community spaces, and also applies when 53 | an individual is officially representing the community in public spaces. 54 | Examples of representing our community include using an official e-mail address, 55 | posting via an official social media account, or acting as an appointed 56 | representative at an online or offline event. 57 | 58 | ## Enforcement 59 | 60 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 61 | reported to the community leaders responsible for enforcement at abuse@score.dev. 62 | 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 117 | 118 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 119 | 120 | For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. 121 | 122 | [homepage]: https://www.contributor-covenant.org 123 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 124 | [Mozilla CoC]: https://github.com/mozilla/diversity 125 | [FAQ]: https://www.contributor-covenant.org/faq 126 | [translations]: https://www.contributor-covenant.org/translations 127 | -------------------------------------------------------------------------------- /internal/command/run.go: -------------------------------------------------------------------------------- 1 | /* 2 | Apache Score 3 | Copyright 2020 The Apache Software Foundation 4 | 5 | This product includes software developed at 6 | The Apache Software Foundation (http://www.apache.org/). 7 | */ 8 | package command 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "io" 14 | "log" 15 | "os" 16 | "strings" 17 | 18 | "github.com/imdario/mergo" 19 | "github.com/mitchellh/mapstructure" 20 | "github.com/spf13/cobra" 21 | "github.com/tidwall/sjson" 22 | "gopkg.in/yaml.v3" 23 | 24 | loader "github.com/score-spec/score-go/loader" 25 | schema "github.com/score-spec/score-go/schema" 26 | score "github.com/score-spec/score-go/types" 27 | 28 | helm "github.com/score-spec/score-helm/internal/helm" 29 | ) 30 | 31 | const ( 32 | scoreFileDefault = "./score.yaml" 33 | overridesFileDefault = "./overrides.score.yaml" 34 | ) 35 | 36 | var ( 37 | scoreFile string 38 | overridesFile string 39 | outFile string 40 | valuesFile string 41 | 42 | overrideParams []string 43 | 44 | skipValidation bool 45 | verbose bool 46 | ) 47 | 48 | func init() { 49 | runCmd.Flags().StringVarP(&scoreFile, "file", "f", scoreFileDefault, "Source SCORE file") 50 | runCmd.Flags().StringVar(&overridesFile, "overrides", overridesFileDefault, "Overrides file") 51 | runCmd.Flags().StringVarP(&valuesFile, "values", "", "", "Imported values file (in YAML format)") 52 | runCmd.Flags().StringVarP(&outFile, "output", "o", "", "Output file") 53 | 54 | runCmd.Flags().StringArrayVarP(&overrideParams, "property", "p", nil, "Overrides selected property value") 55 | 56 | runCmd.Flags().BoolVar(&skipValidation, "skip-validation", false, "DEPRECATED: Disables Score file schema validation") 57 | runCmd.Flags().BoolVar(&verbose, "verbose", false, "Enable diagnostic messages (written to STDERR)") 58 | 59 | rootCmd.AddCommand(runCmd) 60 | } 61 | 62 | var runCmd = &cobra.Command{ 63 | Use: "run", 64 | Short: "Translate SCORE file into Helm values file", 65 | RunE: run, 66 | // we print errors ourselves at the top level 67 | SilenceErrors: true, 68 | } 69 | 70 | func run(cmd *cobra.Command, args []string) error { 71 | // don't print usage if we've parsed the args successfully 72 | cmd.SilenceUsage = true 73 | 74 | if !verbose { 75 | log.SetOutput(io.Discard) 76 | } 77 | 78 | // Open source file 79 | // 80 | var err error 81 | var src *os.File 82 | log.Printf("Reading '%s'...\n", scoreFile) 83 | if src, err = os.Open(scoreFile); err != nil { 84 | return err 85 | } 86 | defer src.Close() 87 | 88 | // Parse SCORE spec 89 | // 90 | log.Print("Parsing SCORE spec...\n") 91 | var srcMap map[string]interface{} 92 | if err := loader.ParseYAML(&srcMap, src); err != nil { 93 | return err 94 | } 95 | 96 | // Apply overrides from file (optional) 97 | // 98 | if overridesFile != "" { 99 | log.Printf("Checking '%s'...\n", overridesFile) 100 | if ovr, err := os.Open(overridesFile); err == nil { 101 | defer ovr.Close() 102 | 103 | log.Print("Applying SCORE overrides...\n") 104 | var ovrMap map[string]interface{} 105 | if err := loader.ParseYAML(&ovrMap, ovr); err != nil { 106 | return err 107 | } 108 | if err := mergo.MergeWithOverwrite(&srcMap, ovrMap); err != nil { 109 | return fmt.Errorf("applying overrides fom '%s': %w", overridesFile, err) 110 | } 111 | } else if !os.IsNotExist(err) || overridesFile != overridesFileDefault { 112 | return err 113 | } 114 | } 115 | 116 | // Apply overrides from command line (optional) 117 | // 118 | for _, pstr := range overrideParams { 119 | log.Print("Applying SCORE properties overrides...\n") 120 | 121 | jsonBytes, err := json.Marshal(srcMap) 122 | if err != nil { 123 | return fmt.Errorf("marshalling score spec: %w", err) 124 | } 125 | 126 | pmap := strings.SplitN(pstr, "=", 2) 127 | if len(pmap) <= 1 { 128 | var path = pmap[0] 129 | log.Printf("removing '%s'", path) 130 | if jsonBytes, err = sjson.DeleteBytes(jsonBytes, path); err != nil { 131 | return fmt.Errorf("removing '%s': %w", path, err) 132 | } 133 | } else { 134 | var path = pmap[0] 135 | var val interface{} 136 | if err := yaml.Unmarshal([]byte(pmap[1]), &val); err != nil { 137 | val = pmap[1] 138 | } 139 | 140 | log.Printf("overriding '%s' = '%s'", path, val) 141 | if jsonBytes, err = sjson.SetBytes(jsonBytes, path, val); err != nil { 142 | return fmt.Errorf("overriding '%s': %w", path, err) 143 | } 144 | } 145 | 146 | if err = json.Unmarshal(jsonBytes, &srcMap); err != nil { 147 | return fmt.Errorf("unmarshalling score spec: %w", err) 148 | } 149 | } 150 | 151 | // Load imported values (optional) 152 | // 153 | var values = make(map[string]interface{}) 154 | if valuesFile != "" { 155 | log.Printf("Importing values from '%s'...\n", valuesFile) 156 | if srcFile, err := os.Open(valuesFile); err != nil { 157 | return err 158 | } else { 159 | defer srcFile.Close() 160 | 161 | if err := yaml.NewDecoder(srcFile).Decode(values); err != nil { 162 | return fmt.Errorf("parsing values file '%s': %w", valuesFile, err) 163 | } 164 | } 165 | } 166 | 167 | // Apply upgrades to fix backports or backward incompatible things 168 | if changes, err := schema.ApplyCommonUpgradeTransforms(srcMap); err != nil { 169 | return fmt.Errorf("failed to upgrade spec: %w", err) 170 | } else if len(changes) > 0 { 171 | for _, change := range changes { 172 | log.Printf("Applying upgrade to specification: %s\n", change) 173 | } 174 | } 175 | 176 | // Validate SCORE spec 177 | // 178 | if !skipValidation { 179 | log.Print("Validating SCORE spec...\n") 180 | if err := schema.Validate(srcMap); err != nil { 181 | return fmt.Errorf("validating workload spec: %w", err) 182 | } 183 | } 184 | 185 | // Convert SCORE spec 186 | // 187 | var spec score.Workload 188 | log.Print("Validating SCORE spec...\n") 189 | if err = mapstructure.Decode(srcMap, &spec); err != nil { 190 | return fmt.Errorf("validating workload spec: %w", err) 191 | } 192 | 193 | // Prepare Helm values 194 | // 195 | var vals = make(map[string]interface{}) 196 | log.Print("Preparing Helm values...\n") 197 | if err = helm.ConvertSpec(vals, &spec, values); err != nil { 198 | return fmt.Errorf("preparing Helm values: %w", err) 199 | } 200 | 201 | // Open output file (optional) 202 | // 203 | var dest = io.Writer(os.Stdout) 204 | if outFile != "" { 205 | log.Printf("Creating '%s'...\n", outFile) 206 | destFile, err := os.Create(outFile) 207 | if err != nil { 208 | return err 209 | } 210 | defer destFile.Close() 211 | 212 | dest = io.MultiWriter(dest, destFile) 213 | } 214 | 215 | // Output Helm values 216 | // 217 | log.Print("Writing Helm values...\n") 218 | if err = helm.WriteYAML(dest, vals); err != nil { 219 | return err 220 | } 221 | 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Humanitec. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------