diff --git a/docs/intro.md b/docs/intro.md index 807edf3fd..6154f1a28 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -290,6 +290,31 @@ targets: image: docker.io/my/custom:mariner2 ``` +### Metadata + +You can include client-side metadata in the spec file. +This may be useful when you want to parse the spec file and do something with your own tooling. + +Any field at the top-level that begins with `x-` will be ignored by the dalec parser. +Any unknown fields besides those that start with `x-` will cause the parser to fail. + +```yaml +soruces: + src: + http: + url: https://example.com/foo.tar.gz + +x-my-custom-field: "foo" +``` + +As an example use-case, you may want to use this to store the targeted image name for a CI/CD pipeline. + +```yaml +x-image-name: "my-package-image:1.0.0" +``` + +Your CI/CD tooling can then parse the spec file and use the `x-image-name` field to tag the built image. + ## Additional Reading * Details on editor support in [editor-support.md](editor-support.md) diff --git a/load.go b/load.go index b6ad8300d..56511067b 100644 --- a/load.go +++ b/load.go @@ -292,6 +292,12 @@ func (s *Spec) SubstituteArgs(env map[string]string) error { // LoadSpec loads a spec from the given data. func LoadSpec(dt []byte) (*Spec, error) { var spec Spec + + dt, err := stripXFields(dt) + if err != nil { + return nil, fmt.Errorf("error stripping x-fields: %w", err) + } + if err := yaml.UnmarshalWithOptions(dt, &spec, yaml.Strict()); err != nil { return nil, fmt.Errorf("error unmarshalling spec: %w", err) } @@ -304,6 +310,21 @@ func LoadSpec(dt []byte) (*Spec, error) { return &spec, nil } +func stripXFields(dt []byte) ([]byte, error) { + var obj map[string]interface{} + if err := yaml.Unmarshal(dt, &obj); err != nil { + return nil, fmt.Errorf("error unmarshalling spec: %w", err) + } + + for k := range obj { + if strings.HasPrefix(k, "x-") { + delete(obj, k) + } + } + + return yaml.Marshal(obj) +} + func (s *BuildStep) processBuildArgs(lex *shell.Lex, args map[string]string, i int) error { for k, v := range s.Env { updated, err := lex.ProcessWordWithMap(v, args) diff --git a/load_test.go b/load_test.go index d172ae33f..f130f58d7 100644 --- a/load_test.go +++ b/load_test.go @@ -399,3 +399,53 @@ func TestSourceNameWithPathSeparator(t *testing.T) { t.Errorf("expected error to be sourceNamePathSeparatorError, got: %v", err) } } + +func TestUnmarshal(t *testing.T) { + t.Run("x-fields are stripped from spec", func(t *testing.T) { + dt := []byte(` +sources: + test: + inline: + file: + contents: "Hello world!" +x-some-field: "some value" +x-some-other-field: "some other value" +`) + + spec, err := LoadSpec(dt) + if err != nil { + t.Fatal(err) + } + + src, ok := spec.Sources["test"] + if !ok { + t.Fatal("expected source to be present") + } + + if src.Inline == nil { + t.Fatal("expected inline source to be present") + } + + if src.Inline.File == nil { + t.Fatal("expected inline file to be present") + } + + const xContents = "Hello world!" + if src.Inline.File.Contents != xContents { + t.Fatalf("expected %q, got %s", xContents, src.Inline.File.Contents) + } + }) + + t.Run("unknown fields cause parse error", func(t *testing.T) { + dt := []byte(` +sources: + test: + noSuchField: "some value" +`) + + _, err := LoadSpec(dt) + if err == nil { + t.Fatal("expected error, but received none") + } + }) +}