diff --git a/pkg/contexts/ocm/utils/localize/README.md b/pkg/contexts/ocm/utils/localize/README.md index 5d20e7549e..8d1c30b344 100644 --- a/pkg/contexts/ocm/utils/localize/README.md +++ b/pkg/contexts/ocm/utils/localize/README.md @@ -47,4 +47,8 @@ resource and further helper parts, like json scheme validation for config files. Such a specification object can be applied by the function `Instantiate` together with configuration values to a component version. As substitution result it returns a virtual filesystem -with the snapshot according to the resolved substitutions. \ No newline at end of file +with the snapshot according to the resolved substitutions. + +Additionally, there is a set of more basic types and methods, which can be used +to describe end execute localizations for single data objects (see `ImageMappings`, +`LocalizeMappings` and `SubstituteMappings`). \ No newline at end of file diff --git a/pkg/contexts/ocm/utils/localize/format.go b/pkg/contexts/ocm/utils/localize/format.go index 6b735b6bf3..fd1fd50e14 100644 --- a/pkg/contexts/ocm/utils/localize/format.go +++ b/pkg/contexts/ocm/utils/localize/format.go @@ -7,8 +7,12 @@ package localize import ( "encoding/json" "fmt" + "strings" + "github.com/open-component-model/ocm/pkg/contexts/ocm" v1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + "github.com/open-component-model/ocm/pkg/contexts/ocm/utils" + "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/runtime" ) @@ -25,6 +29,9 @@ import ( // ImageMapping describes a dedicated substitution of parts // of container image names based on a relative OCM resource reference. type ImageMapping struct { + // The optional but unique(!) name of the mapping to support referencing mapping entries + Name string `json:"name,omitempty"` + // The resource reference used to resolve the substitution v1.ResourceReference `json:",inline"` @@ -38,6 +45,73 @@ type ImageMapping struct { Image string `json:"image,omitempty"` } +type ImageMappings []ImageMapping + +func (m *ImageMapping) Evaluate(idx int, cv ocm.ComponentVersionAccess, resolver ocm.ComponentVersionResolver) (ValueMappings, error) { + name := "image mapping" + if m.Name != "" { + name = fmt.Sprintf("%s %q", name, m.Name) + } else { //nolint: gocritic // yes + if idx >= 0 { + name = fmt.Sprintf("%s %d", name, idx+1) + } + } + acc, rcv, err := utils.ResolveResourceReference(cv, m.ResourceReference, resolver) + if err != nil { + return nil, errors.ErrNotFoundWrap(err, "mapping", fmt.Sprintf("%s (%s)", name, &m.ResourceReference)) + } + rcv.Close() + ref, err := utils.GetOCIArtifactRef(cv.GetContext(), acc) + if err != nil { + return nil, errors.Wrapf(err, "mapping %s: cannot resolve resource %s to an OCI Reference", name, &m.ResourceReference) + } + ix := strings.Index(ref, ":") + if ix < 0 { + ix = strings.Index(ref, "@") + if ix < 0 { + return nil, errors.Wrapf(err, "mapping %s: image tag or digest missing (%s)", name, ref) + } + } + repo := ref[:ix] + tag := ref[ix+1:] + + cnt := 0 + if m.Repository != "" { + cnt++ + } + if m.Tag != "" { + cnt++ + } + if m.Image != "" { + cnt++ + } + if cnt == 0 { + return nil, fmt.Errorf("no substitution target given for %s", name) + } + + var result ValueMappings + var r *ValueMapping + if m.Repository != "" { + if r, err = NewValueMapping(substitutionName(name, "repository", cnt), m.Repository, repo); err != nil { + return nil, errors.Wrapf(err, "setting repository for %s", substitutionName(name, "repository", cnt)) + } + result = append(result, *r) + } + if m.Tag != "" { + if r, err = NewValueMapping(substitutionName(name, "tag", cnt), m.Tag, tag); err != nil { + return nil, errors.Wrapf(err, "setting tag for %s", substitutionName(name, "tag", cnt)) + } + result = append(result, *r) + } + if m.Image != "" { + if r, err = NewValueMapping(substitutionName(name, "image", cnt), m.Image, ref); err != nil { + return nil, errors.Wrapf(err, "setting image for %s", substitutionName(name, "image", cnt)) + } + result = append(result, *r) + } + return result, nil +} + // Localization is a request to substitute an image location. // The specification describes substitution targets given by the file path and // the YAML/JSON value paths of the elements in this file. @@ -45,8 +119,6 @@ type ImageMapping struct { // from the access specification of the given resource provided by the actual // component version. type Localization struct { - // The optional but unique(!) name of the mapping to support referencing mapping entries - Name string `json:"name,omitempty"` // The path of the file for the substitution FilePath string `json:"file"` // The image mapping request @@ -63,6 +135,45 @@ type Localization struct { // value. type Configuration Substitution +type ValueMapping struct { + // The optional but unique(!) name of the mapping to support referencing mapping entries + Name string `json:"name,omitempty"` + // The target path for the value substitution + ValuePath string `json:"path"` + // The value to set + Value json.RawMessage `json:"value"` +} + +func NewValueMapping(name, path string, value interface{}) (*ValueMapping, error) { + var ( + v []byte + err error + ) + + if value != nil { + v, err = runtime.DefaultJSONEncoding.Marshal(value) + if err != nil { + return nil, fmt.Errorf("cannot marshal substitution value: %w", err) + } + } + return &ValueMapping{ + Name: name, + ValuePath: path, + Value: v, + }, nil +} + +type ValueMappings []ValueMapping + +func (s *ValueMappings) Add(name, path string, value interface{}) error { + m, err := NewValueMapping(name, path, value) + if err != nil { + return err + } + *s = append(*s, *m) + return nil +} + // Here comes the structure used for resolved execution requests. // They can be applied to a filesystem content without further external information. // It basically has the same structure as the configuration request, but @@ -73,14 +184,10 @@ type Configuration Substitution // element given by the value path in the given file path by the given // direct value. type Substitution struct { - // The optional but unique(!) name of the mapping to support referencing mapping entries - Name string `json:"name,omitempty"` // The path of the file for the substitution FilePath string `json:"file"` - // The target path for the value substitution - ValuePath string `json:"path"` - // The value to set - Value json.RawMessage `json:"value"` + // The field mapping toapply to given file path + ValueMapping `json:",inline"` } func (s *Substitution) GetValue() (interface{}, error) { @@ -91,24 +198,19 @@ func (s *Substitution) GetValue() (interface{}, error) { type Substitutions []Substitution -func (s *Substitutions) Add(name, file, path string, value interface{}) error { - var ( - v []byte - err error - ) - - if value != nil { - v, err = runtime.DefaultJSONEncoding.Marshal(value) - if err != nil { - return fmt.Errorf("cannot marshal substitution value: %w", err) - } - } +func (s *Substitutions) AddValueMapping(m *ValueMapping, file string) { *s = append(*s, Substitution{ - Name: name, - FilePath: file, - ValuePath: path, - Value: v, + FilePath: file, + ValueMapping: *m, }) +} + +func (s *Substitutions) Add(name, file, path string, value interface{}) error { + m, err := NewValueMapping(name, path, value) + if err != nil { + return err + } + s.AddValueMapping(m, file) return nil } diff --git a/pkg/contexts/ocm/utils/localize/localize.go b/pkg/contexts/ocm/utils/localize/localize.go index 6542dd48dd..df68df9620 100644 --- a/pkg/contexts/ocm/utils/localize/localize.go +++ b/pkg/contexts/ocm/utils/localize/localize.go @@ -5,78 +5,44 @@ package localize import ( - "fmt" - "strings" - "github.com/open-component-model/ocm/pkg/contexts/ocm" - "github.com/open-component-model/ocm/pkg/contexts/ocm/utils" - "github.com/open-component-model/ocm/pkg/errors" ) +// Localize maps a list of filesystem related localization requests to an +// appropriate set of substitution requests. func Localize(mappings []Localization, cv ocm.ComponentVersionAccess, resolver ocm.ComponentVersionResolver) (Substitutions, error) { var result Substitutions - ctx := cv.GetContext() for i, v := range mappings { - name := "image mapping" - if v.Name != "" { - name = fmt.Sprintf("%s %q", name, v.Name) - } - acc, rcv, err := utils.ResolveResourceReference(cv, v.ResourceReference, resolver) - if err != nil { - return nil, errors.ErrNotFoundWrap(err, "mapping", fmt.Sprintf("%d (%s)", i+1, &v.ResourceReference)) - } - rcv.Close() - ref, err := utils.GetOCIArtifactRef(ctx, acc) + m, err := v.Evaluate(i, cv, resolver) if err != nil { - return nil, errors.Wrapf(err, "mapping %d: cannot resolve resource %s to an OCI Reference", i+1, v) + return nil, err } - ix := strings.Index(ref, ":") - if ix < 0 { - ix = strings.Index(ref, "@") - if ix < 0 { - return nil, errors.Wrapf(err, "mapping %d: image tag or digest missing (%s)", i+1, ref) - } + for _, r := range m { + result.AddValueMapping(&r, v.FilePath) } - repo := ref[:ix] - tag := ref[ix+1:] + } + return result, nil +} - cnt := 0 - if v.Repository != "" { - cnt++ - } - if v.Tag != "" { - cnt++ - } - if v.Image != "" { - cnt++ - } - if cnt == 0 { - return nil, fmt.Errorf("no substitution target given for %s", name) - } +// LocalizeMappings maps a set of pure image mappings into +// an appropriate set of value mapping request for a single data object. +func LocalizeMappings(mappings ImageMappings, cv ocm.ComponentVersionAccess, resolver ocm.ComponentVersionResolver) (ValueMappings, error) { + var result ValueMappings - if v.Repository != "" { - if err := result.Add(substitutionName(v.Name, "repository", cnt), v.FilePath, v.Repository, repo); err != nil { - return nil, errors.Wrapf(err, "setting repository for %s", substitutionName(v.Name, "repository", cnt)) - } - } - if v.Tag != "" { - if err := result.Add(substitutionName(v.Name, "tag", cnt), v.FilePath, v.Tag, tag); err != nil { - return nil, errors.Wrapf(err, "setting tag for %s", substitutionName(v.Name, "tag", cnt)) - } - } - if v.Image != "" { - if err := result.Add(substitutionName(v.Name, "image", cnt), v.FilePath, v.Image, ref); err != nil { - return nil, errors.Wrapf(err, "setting image for %s", substitutionName(v.Name, "image", cnt)) - } + for i, v := range mappings { + m, err := v.Evaluate(i, cv, resolver) + if err != nil { + return nil, err } + result = append(result, m...) } return result, nil } func substitutionName(name, sub string, cnt int) string { if name == "" { - return "" + return sub } if cnt <= 1 { return name diff --git a/pkg/contexts/ocm/utils/localize/localize_test.go b/pkg/contexts/ocm/utils/localize/localize_test.go index 182bcf04ef..c93c01b660 100644 --- a/pkg/contexts/ocm/utils/localize/localize_test.go +++ b/pkg/contexts/ocm/utils/localize/localize_test.go @@ -74,7 +74,7 @@ var _ = Describe("image value mapping", func() { subst, err := localize.Localize(mappings, cv, nil) Expect(err).To(Succeed()) Expect(subst).To(Equal(Substitutions(` -- name: test1 +- name: image mapping "test1" file: file1 path: a.b.img value: ghcr.io/mandelsoft/test:v1 @@ -95,15 +95,15 @@ var _ = Describe("image value mapping", func() { subst, err := localize.Localize(mappings, cv, nil) Expect(err).To(Succeed()) Expect(subst).To(Equal(Substitutions(` -- name: test1-repository +- name: image mapping "test1"-repository file: file1 path: a.b.rep value: ghcr.io/mandelsoft/test -- name: test1-tag +- name: image mapping "test1"-tag file: file1 path: a.b.tag value: v1 -- name: test1-image +- name: image mapping "test1"-image file: file1 path: a.b.img value: ghcr.io/mandelsoft/test:v1 diff --git a/pkg/contexts/ocm/utils/localize/subst.go b/pkg/contexts/ocm/utils/localize/subst.go index d6259034a0..b0c9a6f951 100644 --- a/pkg/contexts/ocm/utils/localize/subst.go +++ b/pkg/contexts/ocm/utils/localize/subst.go @@ -49,6 +49,29 @@ func Substitute(subs Substitutions, fs vfs.FileSystem) error { return nil } +// SubstituteMappings substitutes value mappings for a dedicated substitution target. +func SubstituteMappings(subs ValueMappings, target subst.SubstitutionTarget) error { + for i, s := range subs { + if err := target.SubstituteByData(s.ValuePath, s.Value); err != nil { + return errors.Wrapf(err, "entry %d: cannot substitute value", i+1) + } + } + return nil +} + +// SubstituteMappingsForData substitutes value mappings for some data. +func SubstituteMappingsForData(subs ValueMappings, data []byte) ([]byte, error) { + target, err := subst.Parse(data) + if err != nil { + return nil, err + } + err = SubstituteMappings(subs, target) + if err != nil { + return nil, err + } + return target.Content() +} + func Set(content *ast.File, path string, value *ast.File) error { p, err := yaml.PathString("$." + path) if err != nil { diff --git a/pkg/contexts/ocm/utils/localize/target_test.go b/pkg/contexts/ocm/utils/localize/target_test.go new file mode 100644 index 0000000000..5fe0c3811d --- /dev/null +++ b/pkg/contexts/ocm/utils/localize/target_test.go @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package localize_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/testutils" + + "github.com/mandelsoft/vfs/pkg/vfs" + + "github.com/open-component-model/ocm/pkg/common/accessio" + "github.com/open-component-model/ocm/pkg/common/accessobj" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/ociartifact" + v1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" + "github.com/open-component-model/ocm/pkg/contexts/ocm/utils/localize" + "github.com/open-component-model/ocm/pkg/env/builder" +) + +var _ = Describe("value substitution in single target", func() { + + Context("localize", func() { + const ( + ARCHIVE = "archive.ctf" + COMPONENT = "github.com/comp" + VERSION = "1.0.0" + IMAGE = "image" + ) + + var ( + repo ocm.Repository + cv ocm.ComponentVersionAccess + env *builder.Builder + ) + + BeforeEach(func() { + env = builder.NewBuilder(nil) + env.OCMCommonTransport(ARCHIVE, accessio.FormatDirectory, func() { + env.Component(COMPONENT, func() { + env.Version(VERSION, func() { + env.Provider("mandelsoft") + env.Resource(IMAGE, "", "Spiff", v1.LocalRelation, func() { + env.Access(ociartifact.New("ghcr.io/mandelsoft/test:v1")) + }) + }) + }) + }) + + var err error + repo, err = ctf.Open(ocm.DefaultContext(), accessobj.ACC_READONLY, ARCHIVE, 0, env) + Expect(err).To(Succeed()) + + cv, err = repo.LookupComponentVersion(COMPONENT, VERSION) + Expect(err).To(Succeed()) + }) + + AfterEach(func() { + Expect(cv.Close()).To(Succeed()) + Expect(repo.Close()).To(Succeed()) + vfs.Cleanup(env) + }) + + It("uses image ref data from component version", func() { + mappings := ImageMappings(` +- name: test1 + image: a.b.img + resource: + name: image +`) + subst, err := localize.LocalizeMappings(mappings, cv, nil) + Expect(err).To(Succeed()) + Expect(subst).To(Equal(ValueMappings(` +- name: image mapping "test1" + path: a.b.img + value: ghcr.io/mandelsoft/test:v1 +`))) + }) + + It("uses multiple resolved image ref data from component version", func() { + + mappings := ImageMappings(` +- name: test1 + repository: a.b.rep + tag: a.b.tag + image: a.b.img + resource: + name: image +`) + subst, err := localize.LocalizeMappings(mappings, cv, nil) + Expect(err).To(Succeed()) + Expect(subst).To(Equal(ValueMappings(` +- name: image mapping "test1"-repository + path: a.b.rep + value: ghcr.io/mandelsoft/test +- name: image mapping "test1"-tag + path: a.b.tag + value: v1 +- name: image mapping "test1"-image + path: a.b.img + value: ghcr.io/mandelsoft/test:v1 +`))) + }) + }) + + Context("substitute", func() { + var data = []byte(` +manifest: + value1: orig1 + value2: orig2 +`) + + It("handles simple values substitution", func() { + subs := ValueMappings(` +- name: test1 + path: manifest.value1 + value: config1 +- name: test2 + path: manifest.value2 + value: config2 +`) + result, err := localize.SubstituteMappingsForData(subs, data) + Expect(err).To(Succeed()) + + Expect(string(result)).To(StringEqualTrimmedWithContext(` +manifest: + value1: config1 + value2: config2 +`)) + }) + + It("handles json substitution", func() { + subs := ValueMappings(` +- name: test1 + path: manifest.value1 + value: + some: + value: 1 +`) + result, err := localize.SubstituteMappingsForData(subs, data) + Expect(err).To(Succeed()) + + Expect(string(result)).To(StringEqualTrimmedWithContext(` +manifest: + value1: + some: + value: 1 + value2: orig2 +`)) + }) + }) +}) diff --git a/pkg/contexts/ocm/utils/localize/utils_test.go b/pkg/contexts/ocm/utils/localize/utils_test.go index c81e6cc1d1..269d3c79bc 100644 --- a/pkg/contexts/ocm/utils/localize/utils_test.go +++ b/pkg/contexts/ocm/utils/localize/utils_test.go @@ -34,6 +34,18 @@ func Substitutions(data string) localize.Substitutions { return v } +func ImageMappings(data string) localize.ImageMappings { + var v localize.ImageMappings + Expect(runtime.DefaultYAMLEncoding.Unmarshal([]byte(data), &v)).To(Succeed()) + return v +} + +func ValueMappings(data string) localize.ValueMappings { + var v localize.ValueMappings + Expect(runtime.DefaultYAMLEncoding.Unmarshal([]byte(data), &v)).To(Succeed()) + return v +} + func InstRules(data string) *localize.InstantiationRules { var v localize.InstantiationRules Expect(runtime.DefaultYAMLEncoding.Unmarshal([]byte(data), &v)).To(Succeed())