diff --git a/cue/spec.cue b/cue/spec.cue new file mode 100644 index 000000000..d2f9b9c5d --- /dev/null +++ b/cue/spec.cue @@ -0,0 +1,258 @@ +package spec + +import ( + "time" +) + +// TODO: consider adding defaults to more fields + +// This alias exists for the sake of being explicit, and in case we want to add restrictions later +// for what kind of filepaths we allow in Dalec +let filepath = string + +// {0, ..., 511} = {0o000, ..., 0o777} are valid unix perms +let perms = >= 0 & <= 0o777 + +// A source name must be alphanumeric, with the inclusion of '_', '-', and '.' +let sourceName = =~ "^[a-zA-Z0-9-._]+$" + +#BuildStep: { + command: string + env?: [string]: string +} + +#CacheDirConfig: { + mode?: "shared" | "private" | "locked" + key?: string + include_distro_key?: bool + include_arch_key?: bool +} + +#Command: { + dir?: filepath + mounts?: [...#SourceMount] + cache_dirs?: [filepath]: #CacheDirConfig + env?: [string]: string + steps: [...#BuildStep] +} + +#SourceMount: { + dest: filepath + // structural cycle formed by source.image.mounts.spec.source must terminate somehow for cue to accept + // even though there are non-recursive source variants so there is implicitly a base case, + // cue's cycle detection is not currently buggy in this case + spec: null | #Source +} + +#SourceContext: { + name?: string +} + +#SourceDockerImage: { + ref: string + cmd?: #Command +} + +#SourceHTTP: { + // TODO: specify url field further? + url: string +} + +#SourceBuild: { + source?: #SubSource + dockerfile_path?: filepath + target?: string + args?: [string]: string +} + +#SourceGit: { + // TODO: specify URL field further? + url: string + commit?: string + keepGitDir?: bool +} + +#SourceInlineFile: { + contents?: string + permissions?: perms + uid?: >= 0 + gid?: >= 0 +} + +#SourceInlineDir: { + files?: [sourceName]: #SourceInlineFile + permissions?: perms + uid?: >= 0 + gid?: >= 0 +} + +#SourceInlineVariant: { file: #SourceInlineFile } | + { dir: #SourceInlineDir } +#SourceInline: { inline: #SourceInlineVariant } + +#SourceVariant: { context: #SourceContext } | + { git: #SourceGit } | + { build: #SourceBuild } | + { http: #SourceHTTP } | + { image: #SourceDockerImage } | + { inline: #SourceInlineVariant } + +// these are sources which are suitable as a sub-source for +// SourceBuild's .source +#SubSourceVariant: { context: #SourceContext } | + { git: #SourceGit } | + { http: #SourceHTTP } | + { image: #SourceDockerImage } | + #SourceInline + +// base properties which are source-independent +#SourceBase: { path?: filepath + includes?: [...string] + excludes?: [...string] + } + +#Source: {#SourceBase, #SourceVariant} +#SubSource: {#SourceBase, #SubSourceVariant} + +#PackageDependencies: { + build?: [string]: (null | [...string]) + runtime?: [string]: (null | [...string]) + recommends?: [string]: (null | [...string]) + test?: [string]: (null | [...string]) +} + +#ArtifactBuild: { + steps: [...#BuildStep] + env?: [string]: string +} + +#ArtifactConfig: { + subpath?: filepath + name?: string +} + +#Artifacts: { + binaries?: [filepath]: (null | #ArtifactConfig) + manpages?: [filepath]: (null | #ArtifactConfig) +} + +#CheckOutput: { + equals?: string + contains?: [...string] + matches?: string + starts_with?: string + ends_with?: string + empty?: bool + } + +#TestStep: { + command?: string + env?: [string]: string + stdout?: #CheckOutput + stderr?: #CheckOutput + stdin?: string +} + +#FileCheckOutput: { + #CheckOutput + permissions?: perms + is_dir?: bool + not_exist?: bool +} + +#TestSpec: { + name: string + dir?: filepath + mounts?: [...#SourceMount] + cacheDirs?: [string]: #CacheDirConfig + env?: [string]: string + steps: [...#TestStep] + files?: [filepath]: (null | #FileCheckOutput) +} + +#SymlinkTarget: { + path: filepath +} + +#PostInstall: { + symlinks?: [filepath]: #SymlinkTarget +} + +#ImageConfig: { + entrypoint?: string + cmd?: string + env?: [...string] + labels?: [string]: string + volumes?: [string]: null + working_dir?: string + stop_signal?: string + base?: string + post?: #PostInstall + user?: string +} + +#Frontend: { + image: string + cmdline?: string +} + +#Target: { + dependencies?: (null | #PackageDependencies) + image?: #ImageConfig + frontend?: #Frontend + tests?: [...#TestSpec] +} + +#PatchSpec: { + source: sourceName + strip?: int +} + +#ChangelogEntry: { + date: time.Time + author: string + changes: [...string] +} + +#Spec: { + name: string | *"My Package" + description: string | *"My Dalec Package" + website?: string + version: string | *"0.1" + revision: uint | string | *"1" + noarch?: bool + + conflicts?: [string]: (null | [...string]) + replaces?: [string]: (null | [...string]) + provides?: [...string] + + sources?: [sourceName]: #Source + + patches?: [sourceName]: [...#PatchSpec] + + build?: #ArtifactBuild + + // TODO: could probably validate magic variables here, + // TARGET* vars should not have any default values applied + args?: [string]: (string | int | null) + + license: string | *"Needs License" + + vendor: string | *"My Vendor" + + packager: string | *"Azure Container Upstream" + + artifacts?: #Artifacts + + targets?: [string]: #Target + + dependencies?: #PackageDependencies + + image?: #ImageConfig + + changelog?: [...#ChangelogEntry] + + tests?: [...#TestSpec] +} + +#Spec \ No newline at end of file diff --git a/cue_validate_test.go b/cue_validate_test.go new file mode 100644 index 000000000..57deb7fbe --- /dev/null +++ b/cue_validate_test.go @@ -0,0 +1,439 @@ +package dalec + +import ( + "fmt" + "os" + "testing" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/errors" + pkgyaml "cuelang.org/go/pkg/encoding/yaml" + "github.com/google/go-cmp/cmp" +) + +// assumes single cue file path +func withCueModule(t *testing.T, modPath string, runTest func(t *testing.T, cueSpec cue.Value)) { + var c *cue.Context = cuecontext.New() + contents, err := os.ReadFile(modPath) + if err != nil { + t.Fatal(err) + } + + val := c.CompileBytes(contents) + if val.Err() != nil { + t.Error(err) + } + + runTest(t, val) +} + +func sel(v cue.Value, fieldPath string) cue.Value { + return v.LookupPath(cue.ParsePath(fieldPath)) +} + +var testHeader = ` +name: "test-spec" +description: "Test Spec Header" +version: "0.1" +revision: "1" +license: "Apache" +vendor: "Microsoft" +packager: "Azure Container Upstream" +` + +func appendHeader(rest string) string { + return testHeader + "\n" + rest +} + +func assertYamlValidate(t *testing.T, unmarshalInto string, yaml string, wantMsgs []string) { + withCueModule(t, "cue/spec.cue", func(t *testing.T, v cue.Value) { + against := sel(v, unmarshalInto) + fmt.Println(against) + _, err := pkgyaml.Validate([]byte(yaml), against) + + errs := errors.Errors(err) + msgs := []string{} + for _, e := range errs { + msgs = append(msgs, e.Error()) + } + + if !cmp.Equal(msgs, wantMsgs) { + t.Fatalf("Unexpected errors: %v\n", cmp.Diff(errs, wantMsgs)) + } + }) +} + +func TestCueValidate_CacheDirConfig(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "invalid cache dir type", + yaml: `mode: "unknown-mode"`, + unmarshalInto: "#CacheDirConfig", + wantErrs: []string{ + "#CacheDirConfig.mode: 3 errors in empty disjunction:", + `#CacheDirConfig.mode: conflicting values "locked" and "unknown-mode"`, + `#CacheDirConfig.mode: conflicting values "private" and "unknown-mode"`, + `#CacheDirConfig.mode: conflicting values "shared" and "unknown-mode"`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_SourceInlineFile(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "uid < 0", + yaml: `uid: -1`, + unmarshalInto: "#SourceInlineFile", + wantErrs: []string{ + "#SourceInlineFile.uid: invalid value -1 (out of bound >=0)", + }, + }, + { + name: "gid < 0", + yaml: `gid: -1`, + unmarshalInto: "#SourceInlineFile", + wantErrs: []string{ + "#SourceInlineFile.gid: invalid value -1 (out of bound >=0)", + }, + }, + { + name: "invalid permissions", + yaml: `permissions: 999`, + unmarshalInto: "#SourceInlineFile", + wantErrs: []string{ + "#SourceInlineFile.permissions: invalid value 999 (out of bound <=511)", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_SourceInlineDir(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "uid < 0", + yaml: `uid: -1`, + unmarshalInto: "#SourceInlineDir", + wantErrs: []string{ + "#SourceInlineDir.uid: invalid value -1 (out of bound >=0)", + }, + }, + { + name: "gid < 0", + yaml: `gid: -1`, + unmarshalInto: "#SourceInlineDir", + wantErrs: []string{ + "#SourceInlineDir.gid: invalid value -1 (out of bound >=0)", + }, + }, + { + name: "invalid permissions", + yaml: `permissions: 999`, + unmarshalInto: "#SourceInlineDir", + wantErrs: []string{ + "#SourceInlineDir.permissions: invalid value 999 (out of bound <=511)", + }, + }, + { + name: "invalid nested file name", + yaml: ` +files: + my/file: + contents: "some file contents"`, + unmarshalInto: "#SourceInlineDir", + wantErrs: []string{ + `#SourceInlineDir.files."my/file": field not allowed`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_SourceInline(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "file and dir variants both defined", + unmarshalInto: "#SourceInline", + yaml: ` +inline: + file: + contents: "some file contents" + dir: + files: + file1: + contents: "some file contents" +`, + wantErrs: []string{ + "#SourceInline.inline: 2 errors in empty disjunction:", + "#SourceInline.inline.dir: field not allowed", + "#SourceInline.inline.file: field not allowed", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_SourceBuild(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "recursive build", + unmarshalInto: "#SourceBuild", + yaml: ` +source: + build: + source: + inline: + file: + contents: "FROM scratch"`, + wantErrs: []string{ + "#SourceBuild.source.build: field not allowed", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_SourceMount(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "nested source mount", + unmarshalInto: "#SourceMount", + yaml: ` +dest: "/app/sub1" +spec: + image: + ref: "nested-docker-image" + cmd: + dir: "/app/sub2" + mounts: + - dest: "/app/sub2/sub3" + spec: + image: + ref: "nested-docker-image" + cmd: + mounts: + - dest: "/app/sub2/sub3/sub4" + spec: + http: + url: "http://myhost.file.txt" + steps: + - command: "echo hello nested world" + steps: + - command: "echo hello nested world" +`, + wantErrs: []string{}, + }, + { + name: "BUG: null spec in source mount", + unmarshalInto: "#SourceMount", + yaml: ` +dest: "/app/sub1" +spec: +`, + wantErrs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_FileCheckOutput(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "embedded check output", + unmarshalInto: "#FileCheckOutput", + yaml: ` +contains: + - "abcd" + - "efgh" +starts_with: "abcd" +`, + wantErrs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_PatchSpec(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "invalid source name", + unmarshalInto: "#PatchSpec", + yaml: ` +source: mysource* +strip: 1 +`, + wantErrs: []string{ + `#PatchSpec.source: invalid value "mysource*" (out of bound =~"^[a-zA-Z0-9-._]+$")`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_Sources(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "invalid source name character in sources", + yaml: appendHeader( + `sources: + mysrc$: + git: + url: "https://github.com/some-repo.git"`), + unmarshalInto: "#Spec", + wantErrs: []string{"#Spec.sources.mysrc$: field not allowed"}, + }, + { + name: "multiple source types defined", + yaml: ` +git: + url: "https://github.com/some-repo.git" +http: + url: "https://some-site/get-source"`, + unmarshalInto: "#Source", + wantErrs: []string{ + "#Source: 2 errors in empty disjunction:", + "#Source.git: field not allowed", + "#Source.http: field not allowed", + }, + }, + { + name: "source: key provided, no sources defined", + unmarshalInto: "#Spec", + yaml: appendHeader( + `sources: +`), + wantErrs: []string{ + "#Spec.sources: conflicting values null and {[sourceName]:#Source} (mismatched types null and struct)", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} + +func TestCueValidate_ChangelogEntry(t *testing.T) { + tests := []struct { + name string + yaml string + unmarshalInto string + wantErrs []string + }{ + { + name: "invalid date", + yaml: ` +date: invalid-date +author: "John Doe"`, + unmarshalInto: "#ChangelogEntry", + wantErrs: []string{ + `#ChangelogEntry.date: invalid value "invalid-date" (does not satisfy time.Time): error in call to time.Time: invalid time "invalid-date"`, + }, + }, + { + name: "valid date", + yaml: ` +date: "2021-01-01T00:00:00Z" +author: "First Last" +`, + unmarshalInto: "#ChangelogEntry", + wantErrs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertYamlValidate(t, tt.unmarshalInto, tt.yaml, tt.wantErrs) + }) + } +} diff --git a/go.mod b/go.mod index 011259891..96ad13b5c 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 toolchain go1.21.0 require ( + cuelang.org/go v0.7.1 github.com/containerd/containerd v1.7.13 github.com/goccy/go-yaml v1.11.3 github.com/google/go-cmp v0.6.0 @@ -35,6 +36,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/containerd/log v0.1.0 // indirect @@ -53,6 +55,7 @@ require ( github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/in-toto/in-toto-golang v0.5.0 // indirect @@ -65,6 +68,7 @@ require ( github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/signal v0.7.0 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect diff --git a/go.sum b/go.sum index b4734d976..b44f89c72 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,10 @@ cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3h cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cuelabs.dev/go/oci/ociregistry v0.0.0-20231103182354-93e78c079a13 h1:zkiIe8AxZ/kDjqQN+mDKc5BxoVJOqioSdqApjc+eB1I= +cuelabs.dev/go/oci/ociregistry v0.0.0-20231103182354-93e78c079a13/go.mod h1:XGKYSMtsJWfqQYPwq51ZygxAPqpEUj/9bdg16iDPTAA= +cuelang.org/go v0.7.1 h1:wSuUSIKR9M1yrph57l8EJATWVRWHaq/Zd0dFUL10PC8= +cuelang.org/go v0.7.1/go.mod h1:ix+3dM/bSpdG9xg6qpCgnJnpeLtciZu+O/rDbywoMII= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= @@ -32,6 +36,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= @@ -68,6 +74,8 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= +github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -91,6 +99,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= @@ -142,6 +152,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -151,6 +163,8 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/moby/buildkit v0.13.2 h1:nXNszM4qD9E7QtG7bFWPnDI1teUQFQglBzon/IU3SzI= github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -169,6 +183,8 @@ github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= +github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -195,8 +211,10 @@ github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= +github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk= +github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= diff --git a/load.go b/load.go index 630cdab07..d4af96658 100644 --- a/load.go +++ b/load.go @@ -1,18 +1,25 @@ package dalec import ( + _ "embed" goerrors "errors" "fmt" "os" "path" "strings" + "cuelang.org/go/cue/cuecontext" + cueerrors "cuelang.org/go/cue/errors" + pkgyaml "cuelang.org/go/pkg/encoding/yaml" "github.com/goccy/go-yaml" "github.com/moby/buildkit/frontend/dockerfile/shell" "github.com/moby/buildkit/frontend/dockerui" "github.com/pkg/errors" ) +//go:embed cue/spec.cue +var cueSpec string + func knownArg(key string) bool { switch key { case "BUILDKIT_SYNTAX": @@ -270,6 +277,25 @@ func (s *Spec) SubstituteArgs(env map[string]string) error { return nil } +func CueValidate(dt []byte) error { + cueCtx := cuecontext.New() + v := cueCtx.CompileString(cueSpec) + + if v.Err() != nil { + return v.Err() + } + + _, err := pkgyaml.Validate(dt, v) + errs := (cueerrors.Errors(err)) + + var retErrs []error = make([]error, 0, len(errs)) + for _, err := range errs { + retErrs = append(retErrs, err) + } + + return goerrors.Join(retErrs...) +} + // LoadSpec loads a spec from the given data. func LoadSpec(dt []byte) (*Spec, error) { var spec Spec @@ -279,6 +305,10 @@ func LoadSpec(dt []byte) (*Spec, error) { return nil, fmt.Errorf("error stripping x-fields: %w", err) } + if err := CueValidate(dt); err != nil { + return nil, fmt.Errorf("error validating spec with cue: %w", err) + } + if err := yaml.UnmarshalWithOptions(dt, &spec, yaml.Strict()); err != nil { return nil, fmt.Errorf("error unmarshalling spec: %w", err) } diff --git a/test/fixtures/local-context.yml b/test/fixtures/local-context.yml index 527ba20ef..9af7240f0 100644 --- a/test/fixtures/local-context.yml +++ b/test/fixtures/local-context.yml @@ -60,7 +60,7 @@ build: changelog: - - date: 2023-10-10 + date: 2023-10-10T00:00:00Z author: Brian Goff changes: - Added a changelog item diff --git a/test/fixtures/moby-runc.yml b/test/fixtures/moby-runc.yml index 0b0b0fa1b..bacb09e9c 100644 --- a/test/fixtures/moby-runc.yml +++ b/test/fixtures/moby-runc.yml @@ -73,7 +73,7 @@ sources: build: env: - CGO_ENABLED: 1 + CGO_ENABLED: "1" GOGC: off GOFLAGS: -trimpath steps: