diff --git a/loader/loader_test.go b/loader/loader_test.go index 56014ae..111c01e 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -85,7 +85,7 @@ containers: - source: ${resources.data} path: sub/path target: /mnt/data - read_only: true + readOnly: true resources: limits: memory: "128Mi" @@ -155,7 +155,7 @@ resources: { Target: "/etc/hello-world/config.yaml", Mode: stringRef("666"), - Content: "---\n${resources.env.APP_CONFIG}\n", + Content: stringRef("---\n${resources.env.APP_CONFIG}\n"), NoExpand: boolRef(true), }, }, @@ -178,17 +178,17 @@ resources: }, }, LivenessProbe: &types.ContainerProbe{ - HttpGet: &types.HttpProbe{ + HttpGet: types.HttpProbe{ Path: "/alive", - Port: intRef(8080), + Port: 8080, }, }, ReadinessProbe: &types.ContainerProbe{ - HttpGet: &types.HttpProbe{ + HttpGet: types.HttpProbe{ Host: stringRef("1.1.1.1"), Scheme: schemeRef(types.HttpProbeSchemeHTTPS), Path: "/ready", - Port: intRef(8080), + Port: 8080, HttpHeaders: []types.HttpProbeHttpHeadersElem{ {Name: stringRef("Custom-Header"), Value: stringRef("Awesome")}, }, diff --git a/schema/files/Makefile b/schema/files/Makefile new file mode 100644 index 0000000..553ee73 --- /dev/null +++ b/schema/files/Makefile @@ -0,0 +1,45 @@ +# Disable all the default make stuff +MAKEFLAGS += --no-builtin-rules +.SUFFIXES: + +SCORE_EXAMPLES_DIR ?= ./samples/ + +## Display help menu +.PHONY: help +help: + @echo Documented Make targets: + @perl -e 'undef $$/; while (<>) { while ($$_ =~ /## (.*?)(?:\n# .*)*\n.PHONY:\s+(\S+).*/mg) { printf "\033[36m%-30s\033[0m %s\n", $$2, $$1 } }' $(MAKEFILE_LIST) | sort + +# ------------------------------------------------------------------------------ +# NON-PHONY TARGETS +# ------------------------------------------------------------------------------ + +${GOPATH}/bin/jv: +ifeq ($(GOPATH),) + $(error GOPATH must be set) +endif + go install github.com/santhosh-tekuri/jsonschema/cmd/jv@latest + +# ------------------------------------------------------------------------------ +# PHONY TARGETS +# ------------------------------------------------------------------------------ + +.PHONY: .ALWAYS +.ALWAYS: + +## Test that the score schema matches the json-schema reference +.PHONY: test-schema +test-schema: ${GOPATH}/bin/jv + ${GOPATH}/bin/jv -assertformat -assertcontent https://json-schema.org/draft/2020-12/schema ./score-v1b1.json + +## Test that the given score examples in $SCORE_EXAMPLES_DIR match the schema +.PHONY: test-examples +test-examples: ${GOPATH}/bin/jv +ifeq ($(SCORE_EXAMPLES_DIR),) + $(error SCORE_EXAMPLES_DIR must be set) +endif + find ${SCORE_EXAMPLES_DIR} -name 'score*.yaml' -print -exec ${GOPATH}/bin/jv -assertformat -assertcontent ./score-v1b1.json {} \; + +## Run all tests +.PHONY: test +test: test-schema test-examples diff --git a/schema/files/samples/score-full.yaml b/schema/files/samples/score-full.yaml new file mode 100644 index 0000000..46a55a2 --- /dev/null +++ b/schema/files/samples/score-full.yaml @@ -0,0 +1,70 @@ +apiVersion: score.dev/v1b1 +metadata: + name: example-workload-name123 + extra-key: extra-value +service: + ports: + port-one: + port: 1000 + protocol: TCP + targetPort: 10000 + port-two2: + port: 8000 +containers: + container-one1: + image: localhost:4000/repo/my-image:tag + command: ["/bin/sh", "-c"] + args: ["hello", "world"] + resources: + requests: + cpu: 1000m + memory: 10Gi + limits: + cpu: "0.24" + memory: 128M + variables: + SOME_VAR: some content here + files: + - target: /my/file + mode: "0600" + source: file.txt + - target: /my/other/file + content: | + some multiline + content + volumes: + - source: volume-name + target: /mnt/something + path: /sub/path + readOnly: false + - source: volume-two + target: /mnt/something-else + livenessProbe: + httpGet: + port: 8080 + path: /livez + readinessProbe: + httpGet: + host: 127.0.0.1 + port: 80 + scheme: HTTP + path: /readyz + httpHeaders: + - name: SOME_HEADER + value: some-value-here + container-two2: + image: . +resources: + resource-one1: + metadata: + annotations: + Default-Annotation: this is my annotation + prefix.com/Another-Key_Annotation.2: something else + extra-key: extra-value + type: Resource-One + class: default + params: + extra: + data: here + resource-two2: + type: Resource-Two diff --git a/schema/files/score-v1b1.json b/schema/files/score-v1b1.json index bbda8e9..99e8ceb 100644 --- a/schema/files/score-v1b1.json +++ b/schema/files/score-v1b1.json @@ -13,7 +13,8 @@ "properties": { "apiVersion": { "description": "The declared Score Specification version.", - "type": "string" + "type": "string", + "pattern": "^score\\.dev/v1b1$" }, "metadata": { "description": "The metadata description of the Workload.", @@ -24,8 +25,11 @@ "additionalProperties": true, "properties": { "name": { - "description": "A string that can describe the Workload.", - "type": "string" + "description": "A string that can describe the Workload. This must be a valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but may not start or end with '-'.", + "type": "string", + "minLength": 2, + "maxLength": 63, + "pattern": "^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" } } }, @@ -35,9 +39,13 @@ "additionalProperties": false, "properties": { "ports": { - "description": "List of network ports published by the service.", + "description": "The set of named network ports published by the service. The service name must be a valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but may not start or end with '-'.", "type": "object", - "minProperties": 1, + "propertyNames": { + "minLength": 2, + "maxLength": 63, + "pattern": "^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, "additionalProperties": { "$ref": "#/$defs/servicePort" } @@ -45,19 +53,28 @@ } }, "containers": { - "description": "The declared Score Specification version.", + "description": "The declared Score Specification version. The container name must be a valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but may not start or end with '-'.", "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/container" + }, + "propertyNames": { + "minLength": 2, + "maxLength": 63, + "pattern": "^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" } }, "resources": { - "description": "The dependencies needed by the Workload.", + "description": "The Resource dependencies needed by the Workload. The resource name must be a valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but may not start or end with '-'.", "type": "object", - "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/resource" + }, + "propertyNames": { + "minLength": 2, + "maxLength": 63, + "pattern": "^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" } } }, @@ -72,20 +89,28 @@ "properties": { "port": { "description": "The public service port.", - "type": "integer" + "type": "integer", + "minimum": 1, + "maximum": 65535 }, "protocol": { "description": "The transport level protocol. Defaults to TCP.", - "type": "string" + "type": "string", + "enum": [ + "TCP", + "UDP" + ] }, "targetPort": { "description": "The internal service port. This will default to 'port' if not provided.", - "type": "integer" + "type": "integer", + "minimum": 1, + "maximum": 65535 } } }, "resource": { - "description": "The resource name.", + "description": "The set of Resources associated with this Workload.", "type": "object", "additionalProperties": false, "required": [ @@ -93,54 +118,64 @@ ], "properties": { "type": { - "description": "The resource in the target environment.", - "type": "string" + "description": "The Resource type. This should be a type supported by the Score implementations being used.", + "type": "string", + "minLength": 2, + "maxLength": 63, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]$" }, "class": { - "description": "A specialisation of the resource type.", + "description": "A specialisation of the Resource type.", "type": "string", - "pattern": "^[a-z0-9](?:-?[a-z0-9]+)+$" + "minLength": 2, + "maxLength": 63, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]$" }, "metadata": { - "description": "The metadata for the resource.", + "description": "The metadata for the Resource.", "type": "object", - "minProperties": 1, "additionalProperties": true, "properties": { "annotations": { - "description": "Annotations that apply to the property.", + "description": "Annotations that apply to the Resource. The annotation can contain A-Z, a-z, 0-9, and '-' and may contain an optional /-separated RFC1123 Host Name prefix.", "type": "object", - "minProperties": 1, "additionalProperties": { "type": "string" + }, + "propertyNames": { + "minLength": 2, + "maxLength": 316, + "pattern": "^(([a-z0-9][a-z0-9-]{0,61}[a-z0-9])(\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*/)?[A-Za-z0-9][A-Za-z0-9._-]{0,61}[A-Za-z0-9]$" } } } }, "params": { - "description": "The parameters used to validate or provision the resource in the environment.", - "type": "object" + "description": "Optional parameters used to provision the Resource in the environment.", + "type": "object", + "additionalProperties": true } } }, "resourcesLimits": { - "description": "The compute resources limits.", + "description": "The compute and memory resource limits.", "type": "object", - "minProperties": 1, "additionalProperties": false, "properties": { "memory": { - "description": "The memory limit.", - "type": "string" + "description": "The memory limit in bytes with optional unit specifier. For example 125M or 1Gi.", + "type": "string", + "pattern": "^[1-9]\\d*(K|M|G|T|Ki|Mi|Gi|Ti)?$" }, "cpu": { - "description": "The CPU limit.", - "type": "string" + "description": "The CPU limit as whole or fractional CPUs. 'm' indicates milli-CPUs. For example 2 or 125m.", + "type": "string", + "pattern": "^\\d*(?:m|\\.\\d+)?$" } } }, "container": { - "description": "The container name.", + "description": "The specification of a Container within the Workload.", "type": "object", "required": [ "image" @@ -148,21 +183,20 @@ "additionalProperties": false, "properties": { "image": { - "description": "The image name and tag.", - "type": "string" + "description": "The container image name and tag.", + "type": "string", + "minLength": 1 }, "command": { - "description": "If specified, overrides container entry point.", + "description": "If specified, overrides the entrypoint defined in the container image.", "type": "array", - "minItems": 1, "items": { "type": "string" } }, "args": { - "description": "If specified, overrides container entry point arguments.", + "description": "If specified, overrides the arguments passed to the container entrypoint.", "type": "array", - "minItems": 1, "items": { "type": "string" } @@ -170,28 +204,33 @@ "variables": { "description": "The environment variables for the container.", "type": "object", - "minProperties": 1, + "propertyNames": { + "minLength": 1, + "pattern": "^[^=]+$" + }, "additionalProperties": { "type": "string" } }, "files": { - "description": "The extra files to mount.", + "description": "The extra files to mount into the container.", "type": "array", - "minItems": 1, "items": { "type": "object", "required": [ "target" ], + "additionalProperties": false, "properties": { "target": { - "description": "The file path and name.", - "type": "string" + "description": "The file path to expose in the container.", + "type": "string", + "minLength": 1 }, "mode": { - "description": "The file access mode.", - "type": "string" + "description": "The optional file access mode in octal encoding. For example 0600.", + "type": "string", + "pattern": "^0?[0-7]{3}$" }, "source": { "description": "The relative or absolute path to the content file.", @@ -200,19 +239,7 @@ }, "content": { "description": "The inline content for the file.", - "anyOf": [ - { - "type": "string" - }, - { - "deprecated": true, - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ] + "type": "string" }, "noExpand": { "description": "If set to true, the placeholders expansion will not occur in the contents of the file.", @@ -222,11 +249,13 @@ "oneOf": [ { "required": [ + "target", "content" ] }, { "required": [ + "target", "source" ] } @@ -236,9 +265,9 @@ "volumes": { "description": "The volumes to mount.", "type": "array", - "minItems": 1, "items": { "type": "object", + "additionalProperties": false, "required": [ "source", "target" @@ -256,7 +285,7 @@ "description": "The target mount on the container.", "type": "string" }, - "read_only": { + "readOnly": { "description": "Indicates if the volume should be mounted in a read-only mode.", "type": "boolean" } @@ -266,7 +295,6 @@ "resources": { "description": "The compute resources for the container.", "type": "object", - "minProperties": 1, "additionalProperties": false, "properties": { "limits": { @@ -291,7 +319,9 @@ }, "containerProbe": { "type": "object", - "minProperties": 1, + "required": [ + "httpGet" + ], "additionalProperties": false, "properties": { "httpGet": { @@ -304,11 +334,12 @@ "type": "object", "additionalProperties": false, "required": [ + "port", "path" ], "properties": { "host": { - "description": "Host name to connect to. Defaults to the container IP.", + "description": "Host name to connect to. Defaults to the workload IP. The is equivalent to a Host HTTP header.", "type": "string" }, "scheme": { @@ -320,24 +351,26 @@ ] }, "path": { - "description": "The path of the HTTP probe endpoint.", + "description": "The path to access on the HTTP server.", "type": "string" }, "port": { - "description": "The path of the HTTP probe endpoint.", - "type": "integer" + "description": "The port to access on the workload.", + "type": "integer", + "minimum": 1, + "maximum": 65535 }, "httpHeaders": { "description": "Additional HTTP headers to send with the request", "type": "array", - "minItems": 1, "items": { "type": "object", "additionalProperties": false, "properties": { "name": { "description": "The HTTP header name.", - "type": "string" + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$" }, "value": { "description": "The HTTP header value.", diff --git a/schema/schema_test.go b/schema/schema_test.go index 862cc5e..ca2dcb8 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -46,7 +46,7 @@ containers: - source: ${resources.data} path: sub/path target: /mnt/data - read_only: true + readOnly: true resources: limits: memory: "128Mi" @@ -211,7 +211,7 @@ func TestSchema(t *testing.T) { } return src }(), - Message: "/service/ports", + Message: "", }, { Name: "service.ports.* is not set", @@ -398,7 +398,7 @@ func TestSchema(t *testing.T) { hello["command"] = []interface{}{} return src }(), - Message: "/containers/hello/command", + Message: "", }, // containers.*.args @@ -421,7 +421,7 @@ func TestSchema(t *testing.T) { hello["args"] = []interface{}{} return src }(), - Message: "/containers/hello/args", + Message: "", }, // containers.*.variables @@ -444,7 +444,7 @@ func TestSchema(t *testing.T) { hello["variables"] = map[string]interface{}{} return src }(), - Message: "/containers/hello/variables", + Message: "", }, { Name: "containers.*.variables.* is not set", @@ -487,7 +487,7 @@ func TestSchema(t *testing.T) { hello["files"] = []interface{}{} return src }(), - Message: "/containers/hello/files", + Message: "", }, { Name: "containers.*.files.*.target is missing", @@ -681,7 +681,7 @@ func TestSchema(t *testing.T) { } return src }(), - Message: "", + Message: "/containers/hello/files/0/content", }, // containers.*.volumes @@ -704,7 +704,7 @@ func TestSchema(t *testing.T) { hello["volumes"] = []interface{}{} return src }(), - Message: "/containers/hello/volumes", + Message: "", }, { Name: "containers.*.volumes.*.source is missing", @@ -795,26 +795,26 @@ func TestSchema(t *testing.T) { Message: "/containers/hello/volumes/0/target", }, { - Name: "containers.*.volumes.*.read_only is not set", + Name: "containers.*.volumes.*.readOnly is not set", Src: func() map[string]interface{} { src := newTestDocument() var hello = src["containers"].(map[string]interface{})["hello"].(map[string]interface{}) var volumes = hello["volumes"].([]interface{})[0].(map[string]interface{}) - volumes["read_only"] = nil + volumes["readOnly"] = nil return src }(), - Message: "/containers/hello/volumes/0/read_only", + Message: "/containers/hello/volumes/0/readOnly", }, { - Name: "containers.*.volumes.*.read_only is not a boolean", + Name: "containers.*.volumes.*.readOnly is not a boolean", Src: func() map[string]interface{} { src := newTestDocument() var hello = src["containers"].(map[string]interface{})["hello"].(map[string]interface{}) var volumes = hello["volumes"].([]interface{})[0].(map[string]interface{}) - volumes["read_only"] = 12 + volumes["readOnly"] = 12 return src }(), - Message: "/containers/hello/volumes/0/read_only", + Message: "/containers/hello/volumes/0/readOnly", }, // containers.*.resources @@ -837,7 +837,7 @@ func TestSchema(t *testing.T) { hello["resources"] = map[string]interface{}{} return src }(), - Message: "/containers/hello/resources", + Message: "", }, { Name: "containers.*.resources.limits is not set", @@ -857,7 +857,7 @@ func TestSchema(t *testing.T) { hello["resources"].(map[string]interface{})["limits"] = map[string]interface{}{} return src }(), - Message: "/containers/hello/resources/limits", + Message: "", }, { Name: "containers.*.resources.limits.memory is not set", @@ -921,7 +921,7 @@ func TestSchema(t *testing.T) { hello["resources"].(map[string]interface{})["requests"] = map[string]interface{}{} return src }(), - Message: "/containers/hello/resources/requests", + Message: "", }, { Name: "containers.*.resources.requests.memory is not set", @@ -1141,7 +1141,7 @@ func TestSchema(t *testing.T) { httpGet["httpHeaders"] = []interface{}{} return src }(), - Message: "/containers/hello/livenessProbe/httpGet/httpHeaders", + Message: "", }, { Name: "containers.*.livenessProbe.httpGet.httpHeaders.*.name is not set", @@ -1315,7 +1315,7 @@ func TestSchema(t *testing.T) { httpGet["httpHeaders"] = []interface{}{} return src }(), - Message: "/containers/hello/readinessProbe/httpGet/httpHeaders", + Message: "", }, { Name: "containers.*.readinessProbe.httpGet.httpHeaders.*.name is not set", @@ -1400,7 +1400,7 @@ func TestSchema(t *testing.T) { src["resources"] = map[string]interface{}{} return src }(), - Message: "/resources", + Message: "", }, { Name: "resources.* is not set", @@ -1477,7 +1477,7 @@ func TestSchema(t *testing.T) { db["metadata"] = map[string]interface{}{} return src }(), - Message: "/resources/db/metadata", + Message: "", }, // resources.*.metadata.annotations @@ -1502,7 +1502,7 @@ func TestSchema(t *testing.T) { metadata["annotations"] = map[string]interface{}{} return src }(), - Message: "/resources/db/metadata/annotations", + Message: "", }, { Name: "resources.*.metadata.annotations.* is not set", diff --git a/schema/validate.go b/schema/validate.go index 3998265..4ac3987 100644 --- a/schema/validate.go +++ b/schema/validate.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "github.com/santhosh-tekuri/jsonschema/v5" "gopkg.in/yaml.v3" @@ -56,3 +57,61 @@ func Validate(src interface{}) error { return schema.Validate(src) } + +// ApplyCommonUpgradeTransforms when we fix aspects of the score spec over time, we sometimes need to break compatibility. +// To reduce affects on users, we can apply a sequence of transformations to the yaml decoded structure so that we can +// fix things on their behalf. This reduces the impact on existing workflows. This function returns messages regarding +// any changes it has made or an error if the structure was unexpected. +// NOTE: this method should only be used for tools or utilities where there is already an established use-case and +// workflow for example score-compose and score-humanitec. +func ApplyCommonUpgradeTransforms(rawScore map[string]interface{}) ([]string, error) { + changes := make([]string, 0) + + if containersStruct, ok := rawScore["containers"].(map[string]interface{}); ok { + for name, rawContainerStruct := range containersStruct { + containerStruct, ok := rawContainerStruct.(map[string]interface{}) + if !ok { + continue + } + + // We no longer support multi-line content. Update any arrays in line to be newline-separated + if filesStruct, ok := containerStruct["files"].([]interface{}); ok { + for i, rawFileStruct := range filesStruct { + fileStruct, ok := rawFileStruct.(map[string]interface{}) + if !ok { + continue + } + if before, ok := fileStruct["content"].([]interface{}); ok { + delete(fileStruct, "content") + sb := new(strings.Builder) + for il, line := range before { + if il > 0 { + sb.WriteRune('\n') + } + sb.WriteString(fmt.Sprint(line)) + } + fileStruct["content"] = sb.String() + changes = append(changes, fmt.Sprintf("containers.%s.files.%d.content: converted from array", name, i)) + } + } + } + + // We have fixed the naming of the read_only field. It is now readOnly. + if volumesStruct, ok := containerStruct["volumes"].([]interface{}); ok { + for i, rawVolumeStruct := range volumesStruct { + volumeStruct, ok := rawVolumeStruct.(map[string]interface{}) + if !ok { + continue + } + if before, ok := volumeStruct["read_only"].(bool); ok { + delete(volumeStruct, "read_only") + volumeStruct["readOnly"] = before + changes = append(changes, fmt.Sprintf("containers.%s.volumes.%d.read_only: migrated to readOnly", name, i)) + } + } + } + } + } + + return changes, nil +} diff --git a/schema/validate_test.go b/schema/validate_test.go index 918ea6a..259902f 100644 --- a/schema/validate_test.go +++ b/schema/validate_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestValidateYaml(t *testing.T) { @@ -44,7 +45,7 @@ containers: - source: ${resources.data} path: sub/path target: /mnt/data - read_only: true + readOnly: true resources: limits: memory: "128Mi" @@ -122,7 +123,7 @@ containers: - source: ${resources.data} path: sub/path target: /mnt/data - read_only: true + readOnly: true resources: limits: memory: "128Mi" @@ -207,7 +208,7 @@ func TestValidateJson(t *testing.T) { "source": "${resources.data}", "path": "sub/path", "target": "/mnt/data", - "read_only": true + "readOnly": true } ], "resources": { @@ -315,7 +316,7 @@ func TestValidateJson_Error(t *testing.T) { "source": "${resources.data}", "path": "sub/path", "target": "/mnt/data", - "read_only": true + "readOnly": true } ], "resources": { @@ -398,3 +399,43 @@ containers: err := ValidateYaml(bytes.NewReader(source)) assert.EqualError(t, err, "jsonschema: '/metadata' does not validate with https://score.dev/schemas/score#/properties/metadata/required: missing properties: 'name'") } + +func TestApplyCommonUpgradeTransforms(t *testing.T) { + var source = []byte(` +--- +apiVersion: score.dev/v1b1 +metadata: + name: hello-world +containers: + hello: + image: busybox + files: + - target: /etc/hello-world/config.yaml + mode: "666" + content: + - line1 + - line2 + volumes: + - source: ${resources.data} + target: /mnt/data + read_only: true +`) + + var obj map[string]interface{} + var dec = yaml.NewDecoder(bytes.NewReader(source)) + assert.NoError(t, dec.Decode(&obj)) + + // first validation attempt should fail + assert.Error(t, Validate(obj)) + + // apply transforms + changes, err := ApplyCommonUpgradeTransforms(obj) + assert.NoError(t, err) + assert.Len(t, changes, 2) + + // second validation attempt should succeed + assert.NoError(t, Validate(obj)) + + assert.Equal(t, "line1\nline2", obj["containers"].(map[string]interface{})["hello"].(map[string]interface{})["files"].([]interface{})[0].(map[string]interface{})["content"]) + assert.Equal(t, true, obj["containers"].(map[string]interface{})["hello"].(map[string]interface{})["volumes"].([]interface{})[0].(map[string]interface{})["readOnly"]) +} diff --git a/types/types.gen.go b/types/types.gen.go index fe5cb49..722eabe 100644 --- a/types/types.gen.go +++ b/types/types.gen.go @@ -6,41 +6,52 @@ import "encoding/json" import "fmt" import "reflect" -// The container name. -type Container struct { - // If specified, overrides container entry point arguments. - Args []string `json:"args,omitempty" yaml:"args,omitempty" mapstructure:"args,omitempty"` - - // If specified, overrides container entry point. - Command []string `json:"command,omitempty" yaml:"command,omitempty" mapstructure:"command,omitempty"` - - // The extra files to mount. - Files []ContainerFilesElem `json:"files,omitempty" yaml:"files,omitempty" mapstructure:"files,omitempty"` - - // The image name and tag. - Image string `json:"image" yaml:"image" mapstructure:"image"` +// UnmarshalJSON implements json.Unmarshaler. +func (j *Container) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["image"]; !ok || v == nil { + return fmt.Errorf("field image in Container: required") + } + type Plain Container + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if len(plain.Image) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "image", 1) + } + *j = Container(plain) + return nil +} - // The liveness probe for the container. - LivenessProbe *ContainerProbe `json:"livenessProbe,omitempty" yaml:"livenessProbe,omitempty" mapstructure:"livenessProbe,omitempty"` +// The network port description. +type ServicePort struct { + // The public service port. + Port int `json:"port" yaml:"port" mapstructure:"port"` - // The readiness probe for the container. - ReadinessProbe *ContainerProbe `json:"readinessProbe,omitempty" yaml:"readinessProbe,omitempty" mapstructure:"readinessProbe,omitempty"` + // The transport level protocol. Defaults to TCP. + Protocol *ServicePortProtocol `json:"protocol,omitempty" yaml:"protocol,omitempty" mapstructure:"protocol,omitempty"` - // The compute resources for the container. - Resources *ContainerResources `json:"resources,omitempty" yaml:"resources,omitempty" mapstructure:"resources,omitempty"` + // The internal service port. This will default to 'port' if not provided. + TargetPort *int `json:"targetPort,omitempty" yaml:"targetPort,omitempty" mapstructure:"targetPort,omitempty"` +} - // The environment variables for the container. - Variables ContainerVariables `json:"variables,omitempty" yaml:"variables,omitempty" mapstructure:"variables,omitempty"` +type HttpProbeHttpHeadersElem struct { + // The HTTP header name. + Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - // The volumes to mount. - Volumes []ContainerVolumesElem `json:"volumes,omitempty" yaml:"volumes,omitempty" mapstructure:"volumes,omitempty"` + // The HTTP header value. + Value *string `json:"value,omitempty" yaml:"value,omitempty" mapstructure:"value,omitempty"` } type ContainerFilesElem struct { // The inline content for the file. - Content interface{} `json:"content,omitempty" yaml:"content,omitempty" mapstructure:"content,omitempty"` + Content *string `json:"content,omitempty" yaml:"content,omitempty" mapstructure:"content,omitempty"` - // The file access mode. + // The optional file access mode in octal encoding. For example 0600. Mode *string `json:"mode,omitempty" yaml:"mode,omitempty" mapstructure:"mode,omitempty"` // If set to true, the placeholders expansion will not occur in the contents of @@ -50,108 +61,31 @@ type ContainerFilesElem struct { // The relative or absolute path to the content file. Source *string `json:"source,omitempty" yaml:"source,omitempty" mapstructure:"source,omitempty"` - // The file path and name. + // The file path to expose in the container. Target string `json:"target" yaml:"target" mapstructure:"target"` } -type ContainerProbe struct { - // HttpGet corresponds to the JSON schema field "httpGet". - HttpGet *HttpProbe `json:"httpGet,omitempty" yaml:"httpGet,omitempty" mapstructure:"httpGet,omitempty"` -} - -// The compute resources for the container. -type ContainerResources struct { - // The maximum allowed resources for the container. - Limits *ResourcesLimits `json:"limits,omitempty" yaml:"limits,omitempty" mapstructure:"limits,omitempty"` - - // The minimal resources required for the container. - Requests *ResourcesLimits `json:"requests,omitempty" yaml:"requests,omitempty" mapstructure:"requests,omitempty"` -} - -// The environment variables for the container. -type ContainerVariables map[string]string - -type ContainerVolumesElem struct { - // An optional sub path in the volume. - Path *string `json:"path,omitempty" yaml:"path,omitempty" mapstructure:"path,omitempty"` - - // Indicates if the volume should be mounted in a read-only mode. - ReadOnly *bool `json:"read_only,omitempty" yaml:"read_only,omitempty" mapstructure:"read_only,omitempty"` - - // The external volume reference. - Source string `json:"source" yaml:"source" mapstructure:"source"` - - // The target mount on the container. - Target string `json:"target" yaml:"target" mapstructure:"target"` -} - -// An HTTP probe details. -type HttpProbe struct { - // Host name to connect to. Defaults to the container IP. - Host *string `json:"host,omitempty" yaml:"host,omitempty" mapstructure:"host,omitempty"` - - // Additional HTTP headers to send with the request - HttpHeaders []HttpProbeHttpHeadersElem `json:"httpHeaders,omitempty" yaml:"httpHeaders,omitempty" mapstructure:"httpHeaders,omitempty"` - - // The path of the HTTP probe endpoint. - Path string `json:"path" yaml:"path" mapstructure:"path"` - - // The path of the HTTP probe endpoint. - Port *int `json:"port,omitempty" yaml:"port,omitempty" mapstructure:"port,omitempty"` - - // Scheme to use for connecting to the host (HTTP or HTTPS). Defaults to HTTP. - Scheme *HttpProbeScheme `json:"scheme,omitempty" yaml:"scheme,omitempty" mapstructure:"scheme,omitempty"` -} - -type HttpProbeHttpHeadersElem struct { - // The HTTP header name. - Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - - // The HTTP header value. - Value *string `json:"value,omitempty" yaml:"value,omitempty" mapstructure:"value,omitempty"` -} - -type HttpProbeScheme string - -const HttpProbeSchemeHTTP HttpProbeScheme = "HTTP" -const HttpProbeSchemeHTTPS HttpProbeScheme = "HTTPS" - // UnmarshalJSON implements json.Unmarshaler. -func (j *Resource) UnmarshalJSON(b []byte) error { +func (j *Workload) UnmarshalJSON(b []byte) error { var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { return err } - if v, ok := raw["type"]; !ok || v == nil { - return fmt.Errorf("field type in Resource: required") - } - type Plain Resource - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err + if v, ok := raw["apiVersion"]; !ok || v == nil { + return fmt.Errorf("field apiVersion in Workload: required") } - *j = Resource(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *HttpProbe) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err + if v, ok := raw["containers"]; !ok || v == nil { + return fmt.Errorf("field containers in Workload: required") } - if v, ok := raw["path"]; !ok || v == nil { - return fmt.Errorf("field path in HttpProbe: required") + if v, ok := raw["metadata"]; !ok || v == nil { + return fmt.Errorf("field metadata in Workload: required") } - type Plain HttpProbe + type Plain Workload var plain Plain if err := json.Unmarshal(b, &plain); err != nil { return err } - if plain.HttpHeaders != nil && len(plain.HttpHeaders) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "httpHeaders", 1) - } - *j = HttpProbe(plain) + *j = Workload(plain) return nil } @@ -175,123 +109,212 @@ func (j *HttpProbeScheme) UnmarshalJSON(b []byte) error { return nil } -var enumValues_HttpProbeScheme = []interface{}{ - "HTTP", - "HTTPS", +const HttpProbeSchemeHTTP HttpProbeScheme = "HTTP" +const HttpProbeSchemeHTTPS HttpProbeScheme = "HTTPS" + +// An HTTP probe details. +type HttpProbe struct { + // Host name to connect to. Defaults to the workload IP. The is equivalent to a + // Host HTTP header. + Host *string `json:"host,omitempty" yaml:"host,omitempty" mapstructure:"host,omitempty"` + + // Additional HTTP headers to send with the request + HttpHeaders []HttpProbeHttpHeadersElem `json:"httpHeaders,omitempty" yaml:"httpHeaders,omitempty" mapstructure:"httpHeaders,omitempty"` + + // The path to access on the HTTP server. + Path string `json:"path" yaml:"path" mapstructure:"path"` + + // The port to access on the workload. + Port int `json:"port" yaml:"port" mapstructure:"port"` + + // Scheme to use for connecting to the host (HTTP or HTTPS). Defaults to HTTP. + Scheme *HttpProbeScheme `json:"scheme,omitempty" yaml:"scheme,omitempty" mapstructure:"scheme,omitempty"` } // UnmarshalJSON implements json.Unmarshaler. -func (j *ContainerVolumesElem) UnmarshalJSON(b []byte) error { +func (j *HttpProbe) UnmarshalJSON(b []byte) error { var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { return err } - if v, ok := raw["source"]; !ok || v == nil { - return fmt.Errorf("field source in ContainerVolumesElem: required") + if v, ok := raw["path"]; !ok || v == nil { + return fmt.Errorf("field path in HttpProbe: required") } - if v, ok := raw["target"]; !ok || v == nil { - return fmt.Errorf("field target in ContainerVolumesElem: required") + if v, ok := raw["port"]; !ok || v == nil { + return fmt.Errorf("field port in HttpProbe: required") } - type Plain ContainerVolumesElem + type Plain HttpProbe var plain Plain if err := json.Unmarshal(b, &plain); err != nil { return err } - *j = ContainerVolumesElem(plain) + *j = HttpProbe(plain) return nil } +type ContainerProbe struct { + // HttpGet corresponds to the JSON schema field "httpGet". + HttpGet HttpProbe `json:"httpGet" yaml:"httpGet" mapstructure:"httpGet"` +} + // UnmarshalJSON implements json.Unmarshaler. -func (j *ContainerFilesElem) UnmarshalJSON(b []byte) error { +func (j *ContainerProbe) UnmarshalJSON(b []byte) error { var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { return err } - if v, ok := raw["target"]; !ok || v == nil { - return fmt.Errorf("field target in ContainerFilesElem: required") + if v, ok := raw["httpGet"]; !ok || v == nil { + return fmt.Errorf("field httpGet in ContainerProbe: required") } - type Plain ContainerFilesElem + type Plain ContainerProbe var plain Plain if err := json.Unmarshal(b, &plain); err != nil { return err } - if plain.Source != nil && len(*plain.Source) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "source", 1) - } - *j = ContainerFilesElem(plain) + *j = ContainerProbe(plain) return nil } +// The compute and memory resource limits. +type ResourcesLimits struct { + // The CPU limit as whole or fractional CPUs. 'm' indicates milli-CPUs. For + // example 2 or 125m. + Cpu *string `json:"cpu,omitempty" yaml:"cpu,omitempty" mapstructure:"cpu,omitempty"` + + // The memory limit in bytes with optional unit specifier. For example 125M or + // 1Gi. + Memory *string `json:"memory,omitempty" yaml:"memory,omitempty" mapstructure:"memory,omitempty"` +} + +// The compute resources for the container. +type ContainerResources struct { + // The maximum allowed resources for the container. + Limits *ResourcesLimits `json:"limits,omitempty" yaml:"limits,omitempty" mapstructure:"limits,omitempty"` + + // The minimal resources required for the container. + Requests *ResourcesLimits `json:"requests,omitempty" yaml:"requests,omitempty" mapstructure:"requests,omitempty"` +} + +// The environment variables for the container. +type ContainerVariables map[string]string + +type ContainerVolumesElem struct { + // An optional sub path in the volume. + Path *string `json:"path,omitempty" yaml:"path,omitempty" mapstructure:"path,omitempty"` + + // Indicates if the volume should be mounted in a read-only mode. + ReadOnly *bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty" mapstructure:"readOnly,omitempty"` + + // The external volume reference. + Source string `json:"source" yaml:"source" mapstructure:"source"` + + // The target mount on the container. + Target string `json:"target" yaml:"target" mapstructure:"target"` +} + // UnmarshalJSON implements json.Unmarshaler. -func (j *Container) UnmarshalJSON(b []byte) error { +func (j *ContainerVolumesElem) UnmarshalJSON(b []byte) error { var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { return err } - if v, ok := raw["image"]; !ok || v == nil { - return fmt.Errorf("field image in Container: required") + if v, ok := raw["source"]; !ok || v == nil { + return fmt.Errorf("field source in ContainerVolumesElem: required") } - type Plain Container + if v, ok := raw["target"]; !ok || v == nil { + return fmt.Errorf("field target in ContainerVolumesElem: required") + } + type Plain ContainerVolumesElem var plain Plain if err := json.Unmarshal(b, &plain); err != nil { return err } - if plain.Args != nil && len(plain.Args) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "args", 1) - } - if plain.Command != nil && len(plain.Command) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "command", 1) - } - if plain.Files != nil && len(plain.Files) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "files", 1) - } - if plain.Volumes != nil && len(plain.Volumes) < 1 { - return fmt.Errorf("field %s length: must be >= %d", "volumes", 1) - } - *j = Container(plain) + *j = ContainerVolumesElem(plain) return nil } -// The metadata for the resource. -type ResourceMetadata map[string]interface{} +// The specification of a Container within the Workload. +type Container struct { + // If specified, overrides the arguments passed to the container entrypoint. + Args []string `json:"args,omitempty" yaml:"args,omitempty" mapstructure:"args,omitempty"` -// The parameters used to validate or provision the resource in the environment. -type ResourceParams map[string]interface{} + // If specified, overrides the entrypoint defined in the container image. + Command []string `json:"command,omitempty" yaml:"command,omitempty" mapstructure:"command,omitempty"` + + // The extra files to mount into the container. + Files []ContainerFilesElem `json:"files,omitempty" yaml:"files,omitempty" mapstructure:"files,omitempty"` -// The resource name. + // The container image name and tag. + Image string `json:"image" yaml:"image" mapstructure:"image"` + + // The liveness probe for the container. + LivenessProbe *ContainerProbe `json:"livenessProbe,omitempty" yaml:"livenessProbe,omitempty" mapstructure:"livenessProbe,omitempty"` + + // The readiness probe for the container. + ReadinessProbe *ContainerProbe `json:"readinessProbe,omitempty" yaml:"readinessProbe,omitempty" mapstructure:"readinessProbe,omitempty"` + + // The compute resources for the container. + Resources *ContainerResources `json:"resources,omitempty" yaml:"resources,omitempty" mapstructure:"resources,omitempty"` + + // The environment variables for the container. + Variables ContainerVariables `json:"variables,omitempty" yaml:"variables,omitempty" mapstructure:"variables,omitempty"` + + // The volumes to mount. + Volumes []ContainerVolumesElem `json:"volumes,omitempty" yaml:"volumes,omitempty" mapstructure:"volumes,omitempty"` +} + +type HttpProbeScheme string + +// The metadata for the Resource. +type ResourceMetadata map[string]interface{} + +// The set of Resources associated with this Workload. type Resource struct { - // A specialisation of the resource type. + // A specialisation of the Resource type. Class *string `json:"class,omitempty" yaml:"class,omitempty" mapstructure:"class,omitempty"` - // The metadata for the resource. + // The metadata for the Resource. Metadata ResourceMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty" mapstructure:"metadata,omitempty"` - // The parameters used to validate or provision the resource in the environment. + // Optional parameters used to provision the Resource in the environment. Params ResourceParams `json:"params,omitempty" yaml:"params,omitempty" mapstructure:"params,omitempty"` - // The resource in the target environment. + // The Resource type. This should be a type supported by the Score implementations + // being used. Type string `json:"type" yaml:"type" mapstructure:"type"` } -// The compute resources limits. -type ResourcesLimits struct { - // The CPU limit. - Cpu *string `json:"cpu,omitempty" yaml:"cpu,omitempty" mapstructure:"cpu,omitempty"` - - // The memory limit. - Memory *string `json:"memory,omitempty" yaml:"memory,omitempty" mapstructure:"memory,omitempty"` -} - -// The network port description. -type ServicePort struct { - // The public service port. - Port int `json:"port" yaml:"port" mapstructure:"port"` - - // The transport level protocol. Defaults to TCP. - Protocol *string `json:"protocol,omitempty" yaml:"protocol,omitempty" mapstructure:"protocol,omitempty"` +// Optional parameters used to provision the Resource in the environment. +type ResourceParams map[string]interface{} - // The internal service port. This will default to 'port' if not provided. - TargetPort *int `json:"targetPort,omitempty" yaml:"targetPort,omitempty" mapstructure:"targetPort,omitempty"` +// UnmarshalJSON implements json.Unmarshaler. +func (j *Resource) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["type"]; !ok || v == nil { + return fmt.Errorf("field type in Resource: required") + } + type Plain Resource + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if plain.Class != nil && len(*plain.Class) < 2 { + return fmt.Errorf("field %s length: must be >= %d", "class", 2) + } + if plain.Class != nil && len(*plain.Class) > 63 { + return fmt.Errorf("field %s length: must be <= %d", "class", 63) + } + if len(plain.Type) < 2 { + return fmt.Errorf("field %s length: must be >= %d", "type", 2) + } + if len(plain.Type) > 63 { + return fmt.Errorf("field %s length: must be <= %d", "type", 63) + } + *j = Resource(plain) + return nil } // UnmarshalJSON implements json.Unmarshaler. @@ -312,62 +335,108 @@ func (j *ServicePort) UnmarshalJSON(b []byte) error { return nil } -// The declared Score Specification version. -type WorkloadContainers map[string]Container - -// The metadata description of the Workload. -type WorkloadMetadata map[string]interface{} +// UnmarshalJSON implements json.Unmarshaler. +func (j *ContainerFilesElem) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + if v, ok := raw["target"]; !ok || v == nil { + return fmt.Errorf("field target in ContainerFilesElem: required") + } + type Plain ContainerFilesElem + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if plain.Source != nil && len(*plain.Source) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "source", 1) + } + if len(plain.Target) < 1 { + return fmt.Errorf("field %s length: must be >= %d", "target", 1) + } + *j = ContainerFilesElem(plain) + return nil +} -// The dependencies needed by the Workload. -type WorkloadResources map[string]Resource +// UnmarshalJSON implements json.Unmarshaler. +func (j *ServicePortProtocol) UnmarshalJSON(b []byte) error { + var v string + if err := json.Unmarshal(b, &v); err != nil { + return err + } + var ok bool + for _, expected := range enumValues_ServicePortProtocol { + if reflect.DeepEqual(v, expected) { + ok = true + break + } + } + if !ok { + return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_ServicePortProtocol, v) + } + *j = ServicePortProtocol(v) + return nil +} -// List of network ports published by the service. -type WorkloadServicePorts map[string]ServicePort +type ServicePortProtocol string -// The service that the workload provides. -type WorkloadService struct { - // List of network ports published by the service. - Ports WorkloadServicePorts `json:"ports,omitempty" yaml:"ports,omitempty" mapstructure:"ports,omitempty"` -} +const ServicePortProtocolTCP ServicePortProtocol = "TCP" +const ServicePortProtocolUDP ServicePortProtocol = "UDP" // Score workload specification type Workload struct { // The declared Score Specification version. ApiVersion string `json:"apiVersion" yaml:"apiVersion" mapstructure:"apiVersion"` - // The declared Score Specification version. + // The declared Score Specification version. The container name must be a valid + // RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but may not + // start or end with '-'. Containers WorkloadContainers `json:"containers" yaml:"containers" mapstructure:"containers"` // The metadata description of the Workload. Metadata WorkloadMetadata `json:"metadata" yaml:"metadata" mapstructure:"metadata"` - // The dependencies needed by the Workload. + // The Resource dependencies needed by the Workload. The resource name must be a + // valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but + // may not start or end with '-'. Resources WorkloadResources `json:"resources,omitempty" yaml:"resources,omitempty" mapstructure:"resources,omitempty"` // The service that the workload provides. Service *WorkloadService `json:"service,omitempty" yaml:"service,omitempty" mapstructure:"service,omitempty"` } -// UnmarshalJSON implements json.Unmarshaler. -func (j *Workload) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if v, ok := raw["apiVersion"]; !ok || v == nil { - return fmt.Errorf("field apiVersion in Workload: required") - } - if v, ok := raw["containers"]; !ok || v == nil { - return fmt.Errorf("field containers in Workload: required") - } - if v, ok := raw["metadata"]; !ok || v == nil { - return fmt.Errorf("field metadata in Workload: required") - } - type Plain Workload - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = Workload(plain) - return nil +// The declared Score Specification version. The container name must be a valid +// RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but may not +// start or end with '-'. +type WorkloadContainers map[string]Container + +// The metadata description of the Workload. +type WorkloadMetadata map[string]interface{} + +// The Resource dependencies needed by the Workload. The resource name must be a +// valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' but may +// not start or end with '-'. +type WorkloadResources map[string]Resource + +// The service that the workload provides. +type WorkloadService struct { + // The set of named network ports published by the service. The service name must + // be a valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' + // but may not start or end with '-'. + Ports WorkloadServicePorts `json:"ports,omitempty" yaml:"ports,omitempty" mapstructure:"ports,omitempty"` +} + +// The set of named network ports published by the service. The service name must +// be a valid RFC1123 Label Name of up to 63 characters, including a-z, 0-9, '-' +// but may not start or end with '-'. +type WorkloadServicePorts map[string]ServicePort + +var enumValues_HttpProbeScheme = []interface{}{ + "HTTP", + "HTTPS", +} +var enumValues_ServicePortProtocol = []interface{}{ + "TCP", + "UDP", }