Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simple data localization #307

Merged
merged 1 commit into from
Mar 21, 2023
Merged
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
6 changes: 5 additions & 1 deletion pkg/contexts/ocm/utils/localize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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`).
150 changes: 126 additions & 24 deletions pkg/contexts/ocm/utils/localize/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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"`

Expand All @@ -38,15 +45,80 @@ 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.
// The substitution value is calculated
// 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
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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
}

Expand Down
72 changes: 19 additions & 53 deletions pkg/contexts/ocm/utils/localize/localize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions pkg/contexts/ocm/utils/localize/localize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions pkg/contexts/ocm/utils/localize/subst.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading