Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ default:
## Generate types
.PHONY: generate
generate:
# Then the second modification is to remove the array version of the container files and volumes. Anything based on score-go will just handle the map type.
jq '. as $$a | ."$$defs".container.properties.files |= $$a."$$defs".container.properties.files.oneOf[1] | ."$$defs".container.properties.files.additionalProperties |= $$a."$$defs".container.properties.files.oneOf[1].additionalProperties.allOf[1] | ."$$defs".container.properties.volumes |= $$a."$$defs".container.properties.volumes.oneOf[1] | ."$$defs".container.properties.volumes.additionalProperties |= $$a."$$defs".container.properties.volumes.oneOf[1].additionalProperties.allOf[1] | del(."$$defs".containerFile.properties.target) | del(."$$defs".containerVolume.properties.target)' schema/files/score-v1b1.json > schema/files/score-v1b1.json.for-validation
# Then the second modification is to remove the array version of the container files and volumes while preserving shorthand string forms.
jq '. as $$a | ."$$defs".container.properties.files |= $$a."$$defs".container.properties.files.oneOf[1] | ."$$defs".container.properties.files.additionalProperties = {"oneOf":[{"$$ref":"#/$$defs/containerFile"},{"type":"string"}]} | ."$$defs".container.properties.volumes |= $$a."$$defs".container.properties.volumes.oneOf[1] | ."$$defs".container.properties.volumes.additionalProperties = {"oneOf":[{"$$ref":"#/$$defs/containerVolume"},{"type":"string"}]} | del(."$$defs".containerFile.properties.target) | del(."$$defs".containerVolume.properties.target)' schema/files/score-v1b1.json > schema/files/score-v1b1.json.for-validation
# Unfortunately struct generators don't know how to handle mixed properties and additional properties so we have to strip these out before we generate the structs.
# We still validate with the original specification though.
jq 'walk(if type == "object" and .type == "object" and .additionalProperties == true and (.properties | type) == "object" then (del(.required) | del(.properties)) else . end)' schema/files/score-v1b1.json.for-validation > schema/files/score-v1b1.json.for-generation
Expand Down
13 changes: 8 additions & 5 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ containers:
noExpand: true
/etc/hello-world/binary:
binaryContent: aGVsbG8=
/etc/hello-world/short: |
short content
volumes:
/mnt/data:
source: ${resources.data}
Expand Down Expand Up @@ -165,18 +167,19 @@ resources:
Variables: map[string]string{
"FRIEND": "World!",
},
Files: map[string]types.ContainerFile{
"/etc/hello-world/config.yaml": {
Files: types.ContainerFiles{
"/etc/hello-world/config.yaml": types.ContainerFile{
Mode: stringRef("666"),
Content: stringRef("---\n${resources.env.APP_CONFIG}\n"),
NoExpand: boolRef(true),
},
"/etc/hello-world/binary": {
"/etc/hello-world/binary": types.ContainerFile{
BinaryContent: stringRef("aGVsbG8="),
},
"/etc/hello-world/short": stringRef("short content\n"),
},
Volumes: map[string]types.ContainerVolume{
"/mnt/data": {
Volumes: types.ContainerVolumes{
"/mnt/data": types.ContainerVolume{
Source: "${resources.data}",
Path: stringRef("sub/path"),
ReadOnly: boolRef(true),
Expand Down
115 changes: 101 additions & 14 deletions loader/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"unicode/utf8"

"github.com/score-spec/score-go/types"
Expand All @@ -29,24 +30,110 @@ import (
func Normalize(w *types.Workload, baseDir string) error {
for name, c := range w.Containers {
for target, f := range c.Files {
if f.Source != nil {
raw, err := readFile(baseDir, *f.Source)
if err != nil {
return fmt.Errorf("embedding file '%s' for container '%s': %w", *f.Source, name, err)
}
f.Source = nil
if utf8.Valid(raw) {
content := string(raw)
f.Content = &content
} else {
content := base64.StdEncoding.EncodeToString(raw)
f.BinaryContent = &content
}
c.Files[target] = f
updated, changed, err := normalizeContainerFile(f, baseDir)
if err != nil {
return fmt.Errorf("embedding file '%s' for container '%s': %w", target, name, err)
}
if changed {
c.Files[target] = updated
}
}
}

return nil
}

func normalizeContainerFile(file any, baseDir string) (any, bool, error) {
switch f := file.(type) {
case string:
raw, err := readFile(baseDir, f)
if err != nil {
return nil, false, fmt.Errorf("'%s': %w", f, err)
}
if utf8.Valid(raw) {
return map[string]any{"content": string(raw)}, true, nil
}
return map[string]any{"binaryContent": base64.StdEncoding.EncodeToString(raw)}, true, nil
case map[string]any:
source, ok := f["source"].(string)
if !ok || source == "" {
return file, false, nil
}
raw, err := readFile(baseDir, source)
if err != nil {
return nil, false, fmt.Errorf("'%s': %w", source, err)
}
delete(f, "source")
if utf8.Valid(raw) {
f["content"] = string(raw)
} else {
f["binaryContent"] = base64.StdEncoding.EncodeToString(raw)
}
return f, true, nil
}

v := reflect.ValueOf(file)
if !v.IsValid() {
return file, false, nil
}

if v.Kind() == reflect.Ptr {
if v.IsNil() || v.Elem().Kind() != reflect.Struct {
return file, false, nil
}
copy := reflect.New(v.Elem().Type())
copy.Elem().Set(v.Elem())
changed, err := normalizeContainerFileValue(copy.Elem(), baseDir)
if err != nil || !changed {
return file, changed, err
}
return copy.Interface(), true, nil
}

if v.Kind() != reflect.Struct {
return file, false, nil
}

copy := reflect.New(v.Type()).Elem()
copy.Set(v)
changed, err := normalizeContainerFileValue(copy, baseDir)
if err != nil || !changed {
return file, changed, err
}
return copy.Interface(), true, nil
}

func normalizeContainerFileValue(v reflect.Value, baseDir string) (bool, error) {
sourceField := v.FieldByName("Source")
if !sourceField.IsValid() || sourceField.Kind() != reflect.Ptr || sourceField.IsNil() {
return false, nil
}

sourceValue := sourceField.Elem()
if sourceValue.Kind() != reflect.String {
return false, nil
}

source := sourceValue.String()
raw, err := readFile(baseDir, source)
if err != nil {
return false, fmt.Errorf("'%s': %w", source, err)
}

sourceField.Set(reflect.Zero(sourceField.Type()))
if utf8.Valid(raw) {
return true, setStringPtrField(v.FieldByName("Content"), string(raw))
}
return true, setStringPtrField(v.FieldByName("BinaryContent"), base64.StdEncoding.EncodeToString(raw))
}

func setStringPtrField(field reflect.Value, value string) error {
if !field.IsValid() || field.Kind() != reflect.Ptr || field.Type().Elem().Kind() != reflect.String {
return nil
}
v := reflect.New(field.Type().Elem())
v.Elem().SetString(value)
field.Set(v)
return nil
}

Expand Down
20 changes: 11 additions & 9 deletions loader/normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ func TestNormalize(t *testing.T) {
},
Containers: types.WorkloadContainers{
"hello": types.Container{
Files: map[string]types.ContainerFile{
"/etc/hello-world/config.yaml": {
Files: types.ContainerFiles{
"/etc/hello-world/config.yaml": types.ContainerFile{
Source: stringRef("./test_file.txt"),
Mode: stringRef("666"),
NoExpand: boolRef(true),
},
"/etc/hello-world/binary": {
"/etc/hello-world/binary": types.ContainerFile{
Source: stringRef("./test_binary_file"),
},
"/etc/hello-world/short.txt": stringRef("short content"),
},
},
},
Expand All @@ -61,15 +62,16 @@ func TestNormalize(t *testing.T) {
},
Containers: types.WorkloadContainers{
"hello": types.Container{
Files: map[string]types.ContainerFile{
"/etc/hello-world/config.yaml": {
Files: types.ContainerFiles{
"/etc/hello-world/config.yaml": types.ContainerFile{
Mode: stringRef("666"),
Content: stringRef("Hello World\n"),
NoExpand: boolRef(true),
},
"/etc/hello-world/binary": {
"/etc/hello-world/binary": types.ContainerFile{
BinaryContent: stringRef("XVLOjEyq5FKgHDGMAYMdp+crq4I="),
},
"/etc/hello-world/short.txt": stringRef("short content"),
},
},
},
Expand All @@ -84,8 +86,8 @@ func TestNormalize(t *testing.T) {
},
Containers: types.WorkloadContainers{
"hello": types.Container{
Files: map[string]types.ContainerFile{
"/etc/hello-world/config.yaml": {
Files: types.ContainerFiles{
"/etc/hello-world/config.yaml": types.ContainerFile{
Source: stringRef("./not_existing.txt"),
Mode: stringRef("666"),
NoExpand: boolRef(true),
Expand All @@ -94,7 +96,7 @@ func TestNormalize(t *testing.T) {
},
},
},
Error: errors.New("embedding file './not_existing.txt' for container 'hello': open fixtures/not_existing.txt: no such file or directory"),
Error: errors.New("embedding file '/etc/hello-world/config.yaml' for container 'hello': './not_existing.txt': open fixtures/not_existing.txt: no such file or directory"),
},
}

Expand Down
98 changes: 94 additions & 4 deletions loader/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package loader

import (
"fmt"
"reflect"
"regexp"
"strings"

Expand Down Expand Up @@ -71,8 +72,59 @@ func listAllPlaceholders(workload *types.Workload) []string {
placeholderSet := map[string]struct{}{}
for _, container := range workload.Containers {
for _, file := range container.Files {
if (file.NoExpand == nil || !*file.NoExpand) && file.Content != nil {
for _, placeholder := range allPlaceholdersInString(*file.Content) {
noExpand := false
var content *string

switch f := file.(type) {
case string:
c := f
content = &c
case map[string]any:
if v, ok := f["noExpand"].(bool); ok {
noExpand = v
}
if v, ok := f["content"].(string); ok {
c := v
content = &c
}
default:
rv := reflect.ValueOf(file)
if rv.IsValid() {
if rv.Kind() == reflect.Ptr {
if rv.IsNil() {
break
}
rv = rv.Elem()
}
if rv.Kind() == reflect.Struct {
if nf := rv.FieldByName("NoExpand"); nf.IsValid() {
switch nf.Kind() {
case reflect.Bool:
noExpand = nf.Bool()
case reflect.Ptr:
if !nf.IsNil() && nf.Elem().Kind() == reflect.Bool {
noExpand = nf.Elem().Bool()
}
}
}
if cf := rv.FieldByName("Content"); cf.IsValid() {
switch cf.Kind() {
case reflect.String:
c := cf.String()
content = &c
case reflect.Ptr:
if !cf.IsNil() && cf.Elem().Kind() == reflect.String {
c := cf.Elem().String()
content = &c
}
}
}
}
}
}

if !noExpand && content != nil {
for _, placeholder := range allPlaceholdersInString(*content) {
placeholderSet[placeholder] = struct{}{}
}
}
Expand All @@ -83,8 +135,46 @@ func listAllPlaceholders(workload *types.Workload) []string {
}
}
for _, volume := range container.Volumes {
for _, placeholder := range allPlaceholdersInString(volume.Source) {
placeholderSet[placeholder] = struct{}{}
var source *string

switch v := volume.(type) {
case string:
s := v
source = &s
case map[string]any:
if s, ok := v["source"].(string); ok {
source = &s
}
default:
rv := reflect.ValueOf(volume)
if rv.IsValid() {
if rv.Kind() == reflect.Ptr {
if rv.IsNil() {
break
}
rv = rv.Elem()
}
if rv.Kind() == reflect.Struct {
if sf := rv.FieldByName("Source"); sf.IsValid() {
switch sf.Kind() {
case reflect.String:
s := sf.String()
source = &s
case reflect.Ptr:
if !sf.IsNil() && sf.Elem().Kind() == reflect.String {
s := sf.Elem().String()
source = &s
}
}
}
}
}
}

if source != nil {
for _, placeholder := range allPlaceholdersInString(*source) {
placeholderSet[placeholder] = struct{}{}
}
}
}
}
Expand Down
Loading
Loading