diff --git a/.github/config/wordlist.txt b/.github/config/wordlist.txt index d38b0d4469..5fe1dc541a 100644 --- a/.github/config/wordlist.txt +++ b/.github/config/wordlist.txt @@ -281,6 +281,7 @@ toi toiexecutor toolset transportarchive +transferhandler typehandler typename unmarshaller diff --git a/api/oci/utils.go b/api/oci/utils.go index 33e34d4f2a..0d3e3e07be 100644 --- a/api/oci/utils.go +++ b/api/oci/utils.go @@ -19,10 +19,14 @@ func AsTags(tag string) []string { } func StandardOCIRef(host, repository, version string) string { + return fmt.Sprintf("%s%s%s", host, grammar.RepositorySeparator, RelativeOCIRef(repository, version)) +} + +func RelativeOCIRef(repository, version string) string { sep := grammar.TagSeparator i := strings.Index(version, grammar.DigestSeparator) if i > 1 { - return fmt.Sprintf("%s%s%s%s%s", host, grammar.RepositorySeparator, repository, sep, version) + return fmt.Sprintf("%s%s%s", repository, sep, version) } if ok, _ := artdesc.IsDigest(version); ok { sep = grammar.DigestSeparator @@ -30,7 +34,7 @@ func StandardOCIRef(host, repository, version string) string { sep = "" } } - return fmt.Sprintf("%s%s%s%s%s", host, grammar.RepositorySeparator, repository, sep, version) + return fmt.Sprintf("%s%s%s", repository, sep, version) } func IsIntermediate(spec RepositorySpec) bool { diff --git a/api/ocm/cpi/accspeccpi/interface.go b/api/ocm/cpi/accspeccpi/interface.go index 044fe9b2cd..6809c4a806 100644 --- a/api/ocm/cpi/accspeccpi/interface.go +++ b/api/ocm/cpi/accspeccpi/interface.go @@ -11,10 +11,11 @@ type ( AccessType = internal.AccessType - AccessMethodImpl = internal.AccessMethodImpl - AccessMethod = internal.AccessMethod - AccessSpec = internal.AccessSpec - AccessSpecRef = internal.AccessSpecRef + AccessMethodImpl = internal.AccessMethodImpl + AccessMethod = internal.AccessMethod + UniformAccessSpecInfo = internal.UniformAccessSpecInfo + AccessSpec = internal.AccessSpec + AccessSpecRef = internal.AccessSpecRef HintProvider = internal.HintProvider GlobalAccessProvider = internal.GlobalAccessProvider diff --git a/api/ocm/cpi/interface.go b/api/ocm/cpi/interface.go index cfda6966a1..923470acdb 100644 --- a/api/ocm/cpi/interface.go +++ b/api/ocm/cpi/interface.go @@ -50,6 +50,7 @@ type ( ComponentLister = internal.ComponentLister ComponentAccess = internal.ComponentAccess ComponentVersionAccess = internal.ComponentVersionAccess + UniformAccessSpecInfo = internal.UniformAccessSpecInfo AccessSpec = internal.AccessSpec AccessSpecDecoder = internal.AccessSpecDecoder GenericAccessSpec = internal.GenericAccessSpec @@ -183,6 +184,8 @@ func ToGenericRepositorySpec(spec RepositorySpec) (*GenericRepositorySpec, error type AccessSpecRef = internal.AccessSpecRef +var _ AccessSpec = &AccessSpecRef{} + func NewAccessSpecRef(spec AccessSpec) *AccessSpecRef { return internal.NewAccessSpecRef(spec) } diff --git a/api/ocm/extensions/accessmethods/compose/method.go b/api/ocm/extensions/accessmethods/compose/method.go index 9228d2b1c0..937fe48514 100644 --- a/api/ocm/extensions/accessmethods/compose/method.go +++ b/api/ocm/extensions/accessmethods/compose/method.go @@ -68,6 +68,12 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("Composition blob %s", a.Id) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + } +} + func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool { return true } diff --git a/api/ocm/extensions/accessmethods/github/method.go b/api/ocm/extensions/accessmethods/github/method.go index c92ed2c1fe..1086ef7cbb 100644 --- a/api/ocm/extensions/accessmethods/github/method.go +++ b/api/ocm/extensions/accessmethods/github/method.go @@ -105,6 +105,20 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("GitHub commit %s[%s]", a.RepoURL, a.Commit) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + u, err := url.Parse(a.RepoURL) + if err != nil { + u = &url.URL{} + } + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: u.Hostname(), + Port: u.Port(), + Path: u.Path, + Info: a.Commit, + } +} + func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/helm/method.go b/api/ocm/extensions/accessmethods/helm/method.go index 683952a6b4..37d5697b36 100644 --- a/api/ocm/extensions/accessmethods/helm/method.go +++ b/api/ocm/extensions/accessmethods/helm/method.go @@ -3,6 +3,7 @@ package helm import ( "fmt" "io" + "net/url" "strings" "sync" @@ -63,6 +64,20 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("Helm chart %s:%s in repository %s", a.GetChartName(), a.GetVersion(), a.HelmRepository) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + u, err := url.Parse(a.HelmRepository) + if err != nil { + u = &url.URL{} + } + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: u.Hostname(), + Port: u.Port(), + Path: u.Path, + Info: a.HelmChart, + } +} + func (a *AccessSpec) IsLocal(ctx accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/localblob/method.go b/api/ocm/extensions/accessmethods/localblob/method.go index 83fbcc4b05..2799f29c9a 100644 --- a/api/ocm/extensions/accessmethods/localblob/method.go +++ b/api/ocm/extensions/accessmethods/localblob/method.go @@ -115,6 +115,13 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("Local blob %s[%s]", a.LocalReference, a.ReferenceName) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Info: a.LocalReference, + } +} + func (a *AccessSpec) IsLocal(accspeccpi.Context) bool { return true } diff --git a/api/ocm/extensions/accessmethods/maven/method.go b/api/ocm/extensions/accessmethods/maven/method.go index e37cad5bc1..a9433c7c89 100644 --- a/api/ocm/extensions/accessmethods/maven/method.go +++ b/api/ocm/extensions/accessmethods/maven/method.go @@ -2,6 +2,7 @@ package maven import ( "fmt" + "net/url" "github.com/mandelsoft/goutils/optionutils" @@ -78,6 +79,20 @@ func (a *AccessSpec) Describe(_ accspeccpi.Context) string { return fmt.Sprintf("Maven package '%s' in repository '%s' path '%s'", a.Coordinates.String(), a.RepoUrl, a.Coordinates.FilePath()) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + u, err := url.Parse(a.RepoUrl) + if err != nil { + u = &url.URL{} + } + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: u.Hostname(), + Port: u.Port(), + Path: u.Path, + Info: a.Coordinates.String(), + } +} + func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/none/method.go b/api/ocm/extensions/accessmethods/none/method.go index 7804cd8783..dbfbced391 100644 --- a/api/ocm/extensions/accessmethods/none/method.go +++ b/api/ocm/extensions/accessmethods/none/method.go @@ -43,6 +43,12 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return "none" } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + } +} + func (s *AccessSpec) IsLocal(context accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/npm/method.go b/api/ocm/extensions/accessmethods/npm/method.go index ed944f24fe..c7f0e73c0c 100644 --- a/api/ocm/extensions/accessmethods/npm/method.go +++ b/api/ocm/extensions/accessmethods/npm/method.go @@ -2,6 +2,7 @@ package npm import ( "fmt" + "net/url" "ocm.software/ocm/api/ocm/cpi/accspeccpi" "ocm.software/ocm/api/utils/blobaccess/blobaccess" @@ -49,6 +50,20 @@ func (a *AccessSpec) Describe(_ accspeccpi.Context) string { return fmt.Sprintf("NPM package %s:%s in registry %s", a.Package, a.Version, a.Registry) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + u, err := url.Parse(a.Registry) + if err != nil { + u = &url.URL{} + } + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: u.Hostname(), + Port: u.Port(), + Path: u.Path, + Info: a.GetReferenceHint(nil), + } +} + func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/ociartifact/method.go b/api/ocm/extensions/accessmethods/ociartifact/method.go index 38fef2b5ef..d243c02db9 100644 --- a/api/ocm/extensions/accessmethods/ociartifact/method.go +++ b/api/ocm/extensions/accessmethods/ociartifact/method.go @@ -75,6 +75,25 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("OCI artifact %s", a.ImageReference) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + ref, _ := oci.ParseRef(a.ImageReference) + host, port := ref.HostPort() + + r := ref.Repository + if ref.Tag != nil { + r += ":" + *ref.Tag + } + if ref.Digest != nil { + r += "@" + ref.Digest.String() + } + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: host, + Port: port, + Info: r, + } +} + func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/ociblob/method.go b/api/ocm/extensions/accessmethods/ociblob/method.go index aad99685e0..a38f6033d9 100644 --- a/api/ocm/extensions/accessmethods/ociblob/method.go +++ b/api/ocm/extensions/accessmethods/ociblob/method.go @@ -3,6 +3,7 @@ package ociblob import ( "fmt" "io" + "strings" "sync" "github.com/mandelsoft/goutils/errors" @@ -62,6 +63,22 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("OCI blob %s in repository %s", a.Digest, a.Reference) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + segs := strings.Split(a.Reference, "/") + comps := strings.Split(segs[0], ":") + port := "" + if len(comps) > 1 { + port = comps[1] + } + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: comps[0], + Port: port, + Path: strings.Join(segs[1:], "/"), + Info: a.Digest.String(), + } +} + func (s *AccessSpec) IsLocal(context accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/ocm/method.go b/api/ocm/extensions/accessmethods/ocm/method.go index f48edac666..0e3c16e445 100644 --- a/api/ocm/extensions/accessmethods/ocm/method.go +++ b/api/ocm/extensions/accessmethods/ocm/method.go @@ -77,6 +77,21 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("OCM resource %s%s", a.ResourceRef.String(), comp) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + ref := a.OCMRepository.AsUniformSpec(ctx) + if ref == nil { + return &accspeccpi.UniformAccessSpecInfo{Kind: Type} + } + host, port := ref.HostPort() + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: host, + Port: port, + Path: ref.SubPath, + Info: ref.Info, + } +} + func (a *AccessSpec) IsLocal(ctx accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/plugin/method.go b/api/ocm/extensions/accessmethods/plugin/method.go index 95f8e32eed..9dede0b16b 100644 --- a/api/ocm/extensions/accessmethods/plugin/method.go +++ b/api/ocm/extensions/accessmethods/plugin/method.go @@ -36,6 +36,10 @@ func (s *AccessSpec) Describe(ctx cpi.Context) string { return s.handler.Describe(s, ctx) } +func (s *AccessSpec) Info(ctx cpi.Context) *cpi.UniformAccessSpecInfo { + return s.handler.Info(s, ctx) +} + func (_ *AccessSpec) IsLocal(cpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/plugin/plugin.go b/api/ocm/extensions/accessmethods/plugin/plugin.go index a6465a7224..55ce2f137d 100644 --- a/api/ocm/extensions/accessmethods/plugin/plugin.go +++ b/api/ocm/extensions/accessmethods/plugin/plugin.go @@ -33,7 +33,7 @@ func NewPluginHandler(p plugin.Plugin) *PluginHandler { return &PluginHandler{plug: p} } -func (p *PluginHandler) Info(spec *AccessSpec) (*ppi.AccessSpecInfo, error) { +func (p *PluginHandler) GetAccessSpecInfo(spec *AccessSpec) (*ppi.AccessSpecInfo, error) { if p.info != nil || p.err != nil { raw, err := spec.UnstructuredVersionedTypedObject.GetRaw() if err != nil { @@ -58,7 +58,7 @@ func (p *PluginHandler) AccessMethod(spec *AccessSpec, cv cpi.ComponentVersionAc return nil, err } - info, err := p.Info(spec) + info, err := p.GetAccessSpecInfo(spec) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (p *PluginHandler) AccessMethod(spec *AccessSpec, cv cpi.ComponentVersionAc } func (p *PluginHandler) getCredentialData(spec *AccessSpec, cv cpi.ComponentVersionAccess) (json.RawMessage, error) { - info, err := p.Info(spec) + info, err := p.GetAccessSpecInfo(spec) if err != nil { return nil, err } @@ -94,19 +94,41 @@ func (p *PluginHandler) Describe(spec *AccessSpec, ctx cpi.Context) string { if mspec == nil { return "unknown type " + spec.GetType() } - info, err := p.Info(spec) + info, err := p.GetAccessSpecInfo(spec) if err != nil { return err.Error() } return info.Short } +func (p *PluginHandler) Info(spec *AccessSpec, ctx cpi.Context) *cpi.UniformAccessSpecInfo { + mspec := p.GetAccessMethodDescriptor(spec.GetKind(), spec.GetVersion()) + if mspec == nil { + return &cpi.UniformAccessSpecInfo{ + Kind: spec.GetKind(), + } + } + info, err := p.GetAccessSpecInfo(spec) + if err != nil || info.Info == nil { + return &cpi.UniformAccessSpecInfo{ + Kind: spec.GetKind(), + } + } + return &cpi.UniformAccessSpecInfo{ + Kind: spec.GetKind(), + Host: info.Info.Host, + Port: info.Info.Port, + Path: info.Info.Path, + Info: info.Info.Info, + } +} + func (p *PluginHandler) GetMimeType(spec *AccessSpec) string { mspec := p.GetAccessMethodDescriptor(spec.GetKind(), spec.GetVersion()) if mspec == nil { return "unknown type " + spec.GetType() } - info, err := p.Info(spec) + info, err := p.GetAccessSpecInfo(spec) if err != nil { return "" } @@ -118,7 +140,7 @@ func (p *PluginHandler) GetReferenceHint(spec *AccessSpec, cv cpi.ComponentVersi if mspec == nil { return "unknown type " + spec.GetType() } - info, err := p.Info(spec) + info, err := p.GetAccessSpecInfo(spec) if err != nil { return "" } diff --git a/api/ocm/extensions/accessmethods/relativeociref/method.go b/api/ocm/extensions/accessmethods/relativeociref/method.go index 69a28664a9..9d0d3d5594 100644 --- a/api/ocm/extensions/accessmethods/relativeociref/method.go +++ b/api/ocm/extensions/accessmethods/relativeociref/method.go @@ -44,6 +44,13 @@ func (a *AccessSpec) Describe(context accspeccpi.Context) string { return fmt.Sprintf("local OCI artifact %s", a.Reference) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Info: a.Reference, + } +} + func (a *AccessSpec) IsLocal(context accspeccpi.Context) bool { return true } diff --git a/api/ocm/extensions/accessmethods/s3/method.go b/api/ocm/extensions/accessmethods/s3/method.go index b455ab9f0e..5bce32c0b9 100644 --- a/api/ocm/extensions/accessmethods/s3/method.go +++ b/api/ocm/extensions/accessmethods/s3/method.go @@ -85,9 +85,19 @@ func (a AccessSpec) MarshalJSON() ([]byte, error) { } func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { + if a.Version != "" { + return fmt.Sprintf("S3 key %s(%s) in bucket %s", a.Key, a.Version, a.Bucket) + } return fmt.Sprintf("S3 key %s in bucket %s", a.Key, a.Bucket) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Info: a.Bucket + "/" + a.Key, + } +} + func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool { return false } diff --git a/api/ocm/extensions/accessmethods/wget/method.go b/api/ocm/extensions/accessmethods/wget/method.go index 6196ed0410..8f314e7f95 100644 --- a/api/ocm/extensions/accessmethods/wget/method.go +++ b/api/ocm/extensions/accessmethods/wget/method.go @@ -3,6 +3,7 @@ package wget import ( "fmt" "io" + "net/url" "sync" "github.com/mandelsoft/goutils/optionutils" @@ -70,6 +71,19 @@ func (a *AccessSpec) Describe(ctx accspeccpi.Context) string { return fmt.Sprintf("Files from %s", a.URL) } +func (a *AccessSpec) Info(ctx accspeccpi.Context) *accspeccpi.UniformAccessSpecInfo { + u, err := url.Parse(a.URL) + if err != nil { + u = &url.URL{} + } + return &accspeccpi.UniformAccessSpecInfo{ + Kind: Type, + Host: u.Hostname(), + Port: u.Port(), + Path: u.Path, + } +} + func (a *AccessSpec) IsLocal(ctx accspeccpi.Context) bool { return false } diff --git a/api/ocm/interface.go b/api/ocm/interface.go index b4c5f93d89..6f90298409 100644 --- a/api/ocm/interface.go +++ b/api/ocm/interface.go @@ -48,6 +48,7 @@ type ( ComponentLister = internal.ComponentLister ComponentAccess = internal.ComponentAccess ComponentVersionAccess = internal.ComponentVersionAccess + UniformAccessSpecInfo = internal.UniformAccessSpecInfo AccessSpec = internal.AccessSpec GenericAccessSpec = internal.GenericAccessSpec HintProvider = internal.HintProvider diff --git a/api/ocm/internal/accessspecref.go b/api/ocm/internal/accessspecref.go index 0a96071b11..ab5d0545e3 100644 --- a/api/ocm/internal/accessspecref.go +++ b/api/ocm/internal/accessspecref.go @@ -79,6 +79,16 @@ func (a *AccessSpecRef) Describe(ctx Context) string { return "invalid access specification" } +func (a *AccessSpecRef) Info(ctx Context) *UniformAccessSpecInfo { + a.assure(ctx) + if a.evaluated != nil { + return a.evaluated.Info(ctx) + } + return &UniformAccessSpecInfo{ + Kind: a.GetKind(), + } +} + func (a *AccessSpecRef) GetType() string { if a.evaluated != nil { return a.evaluated.GetType() diff --git a/api/ocm/internal/accesstypes.go b/api/ocm/internal/accesstypes.go index 42f87e6891..07de2be827 100644 --- a/api/ocm/internal/accesstypes.go +++ b/api/ocm/internal/accesstypes.go @@ -12,6 +12,7 @@ import ( "ocm.software/ocm/api/ocm/compdesc" metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/tech" "ocm.software/ocm/api/utils/cobrautils/flagsets/flagsetscheme" "ocm.software/ocm/api/utils/errkind" "ocm.software/ocm/api/utils/refmgmt" @@ -20,6 +21,8 @@ import ( type AccessType flagsetscheme.VersionTypedObjectType[AccessSpec] +type UniformAccessSpecInfo = tech.UniformAccessSpecInfo + // AccessSpec is the interface access method specifications // must fulfill. The main task is to map the specification // to a concrete implementation of the access method for a dedicated @@ -27,6 +30,7 @@ type AccessType flagsetscheme.VersionTypedObjectType[AccessSpec] type AccessSpec interface { compdesc.AccessSpec Describe(Context) string + Info(ctx Context) *UniformAccessSpecInfo IsLocal(Context) bool GlobalAccessSpec(Context) AccessSpec // AccessMethod provides an access method implementation for @@ -146,6 +150,13 @@ func (s *UnknownAccessSpec) Describe(ctx Context) string { return fmt.Sprintf("unknown access method type %q", s.GetType()) } +func (s *UnknownAccessSpec) Info(ctx Context) *UniformAccessSpecInfo { + return &UniformAccessSpecInfo{ + Kind: s.GetKind(), + Info: fmt.Sprintf("unknown access method type %q", s.GetType()), + } +} + func (_ *UnknownAccessSpec) IsLocal(Context) bool { return false } @@ -207,6 +218,16 @@ func (s *GenericAccessSpec) Describe(ctx Context) string { return eff.Describe(ctx) } +func (s *GenericAccessSpec) Info(ctx Context) *UniformAccessSpecInfo { + eff, err := s.Evaluate(ctx) + if err != nil { + return &UniformAccessSpecInfo{ + Kind: s.GetType(), + } + } + return eff.Info(ctx) +} + func (s *GenericAccessSpec) Evaluate(ctx Context) (AccessSpec, error) { raw, err := s.GetRaw() if err != nil { diff --git a/api/ocm/internal/uniform.go b/api/ocm/internal/uniform.go index 3aea8d8aaa..ef694aa128 100644 --- a/api/ocm/internal/uniform.go +++ b/api/ocm/internal/uniform.go @@ -3,6 +3,7 @@ package internal import ( "encoding/json" "fmt" + "strings" "sync" "github.com/mandelsoft/goutils/errors" @@ -47,6 +48,14 @@ func (r *UniformRepositorySpec) CredHost() string { return r.Host } +func (u *UniformRepositorySpec) HostPort() (string, string) { + i := strings.Index(u.Host, ":") + if i < 0 { + return u.Host, "" + } + return u.Host[:i], u.Host[i+1:] +} + func (u *UniformRepositorySpec) String() string { t := u.Type if t != "" { diff --git a/api/ocm/plugin/cache/plugin.go b/api/ocm/plugin/cache/plugin.go index f6412b7a23..6b74dde38d 100644 --- a/api/ocm/plugin/cache/plugin.go +++ b/api/ocm/plugin/cache/plugin.go @@ -232,6 +232,22 @@ func (p *pluginImpl) GetUploaderDescriptor(name string) *descriptor.UploaderDesc return p.descriptor.Uploaders.Get(name) } +func (p *pluginImpl) GetTransferHandler(name string) *descriptor.TransferHandlerDescriptor { + if !p.IsValid() { + return nil + } + + return p.descriptor.TransferHandlers.Get(name) +} + +func (p *pluginImpl) GetTransferHandlerNames() []string { + if !p.IsValid() { + return nil + } + + return p.descriptor.TransferHandlers.GetNames() +} + func (p *pluginImpl) Message() string { if p.IsValid() { return p.descriptor.Short diff --git a/api/ocm/plugin/cache/plugindir.go b/api/ocm/plugin/cache/plugindir.go index a09d1b0bda..54faba5b66 100644 --- a/api/ocm/plugin/cache/plugindir.go +++ b/api/ocm/plugin/cache/plugindir.go @@ -118,7 +118,7 @@ func (c *pluginDirImpl) scan(path string) error { errmsg = err.Error() } else { if desc.PluginName != fi.Name() { - errmsg = fmt.Sprintf("nmatching plugin name %q", desc.PluginName) + errmsg = fmt.Sprintf("no matching plugin name %q, found file %q", desc.PluginName, fi.Name()) } } c.add(fi.Name(), desc, execpath, errmsg, list) diff --git a/api/ocm/plugin/common/describe.go b/api/ocm/plugin/common/describe.go index fa6c0e3165..9fd92a3934 100644 --- a/api/ocm/plugin/common/describe.go +++ b/api/ocm/plugin/common/describe.go @@ -80,6 +80,11 @@ func DescribePluginDescriptorCapabilities(reg api.ActionTypeRegistry, d *descrip out.Printf("Config Types for CLI Command Extensions:\n") DescribeConfigTypes(d, out) } + if len(d.TransferHandlers) > 0 { + out.Printf("\n") + out.Printf("Transfer Handlers:\n") + DescribeTransferHandlers(d, out) + } } type MethodInfo struct { @@ -517,6 +522,70 @@ func DescribeConfigTypes(d *descriptor.Descriptor, out common.Printer) { } } +type TransferHandlerInfo struct { + Name string + Description string + Questions map[string]descriptor.QuestionDescriptor +} + +func GetTransferHandlerInfo(types []descriptor.TransferHandlerDescriptor) map[string]*TransferHandlerInfo { + found := map[string]*TransferHandlerInfo{} + for _, m := range types { + i := found[m.Name] + if i == nil { + i := &TransferHandlerInfo{ + Name: m.Name, + Description: m.Description, + Questions: map[string]descriptor.QuestionDescriptor{}, + } + for _, q := range m.Questions { + i.Questions[q.Question] = q + } + found[m.Name] = i + } + } + return found +} + +func DescribeTransferHandlers(d *descriptor.Descriptor, out common.Printer) { + types := GetTransferHandlerInfo(d.TransferHandlers) + for _, n := range utils.StringMapKeys(types) { + out.Printf("- Name: %s\n", n) + m := types[n] + if m.Description != "" { + out.Printf("%s\n", utils.IndentLines(m.Description, " ")) + } + out := out.AddGap(" ") + out.Printf("Questions:\n") + for _, qn := range utils.StringMapKeys(m.Questions) { + desc := TransferHandlerQuestions[qn] + if desc == "" { + desc = "invalid question" + } + out.Printf("- Name: %s (%s)\n", qn, desc) + q := m.Questions[qn] + if q.Description != "" { + out.Printf("%s\n", utils.IndentLines(q.Description, " ")) + } + out := out.AddGap(" ") + if q.Labels == nil { + out.Printf("consumes all labels\n") + } else { + if len(*q.Labels) == 0 { + out.Printf("consumes no labels\n") + } else { + labels := slices.Clone(*q.Labels) + sort.Strings(labels) + out.Printf("Labels:\n") + for _, l := range labels { + out.Printf("- %s\n", l) + } + } + } + } + } +} + type Describable interface { Describe() string } diff --git a/api/ocm/plugin/common/questions.go b/api/ocm/plugin/common/questions.go new file mode 100644 index 0000000000..a2a7171990 --- /dev/null +++ b/api/ocm/plugin/common/questions.go @@ -0,0 +1,23 @@ +package common + +import ( + "ocm.software/ocm/api/ocm/plugin/internal" +) + +const ( + Q_UPDATE_VERSION = internal.Q_UPDATE_VERSION + Q_OVERWRITE_VERSION = internal.Q_OVERWRITE_VERSION + Q_ENFORCE_TRANSPORT = internal.Q_ENFORCE_TRANSPORT + Q_TRANSFER_VERSION = internal.Q_TRANSFER_VERSION + Q_TRANSFER_RESOURCE = internal.Q_TRANSFER_RESOURCE + Q_TRANSFER_SOURCE = internal.Q_TRANSFER_SOURCE +) + +var TransferHandlerQuestions = map[string]string{ + Q_UPDATE_VERSION: "update non-signature relevant parts", + Q_ENFORCE_TRANSPORT: "enforce transport as component version does not exist in target ", + Q_OVERWRITE_VERSION: "update signature relevant parts", + Q_TRANSFER_VERSION: "decide on updating a component version", + Q_TRANSFER_RESOURCE: "value transport for resources", + Q_TRANSFER_SOURCE: "value transport for sources", +} diff --git a/api/ocm/plugin/descriptor/const.go b/api/ocm/plugin/descriptor/const.go index d2becd18a5..d13af20f56 100644 --- a/api/ocm/plugin/descriptor/const.go +++ b/api/ocm/plugin/descriptor/const.go @@ -7,13 +7,14 @@ import ( ) const ( - KIND_PLUGIN = "plugin" - KIND_DOWNLOADER = "downloader" - KIND_UPLOADER = "uploader" - KIND_ACCESSMETHOD = errkind.KIND_ACCESSMETHOD - KIND_ACTION = action.KIND_ACTION - KIND_VALUESET = "value set" - KIND_PURPOSE = "purposet" + KIND_PLUGIN = "plugin" + KIND_DOWNLOADER = "downloader" + KIND_UPLOADER = "uploader" + KIND_ACCESSMETHOD = errkind.KIND_ACCESSMETHOD + KIND_ACTION = action.KIND_ACTION + KIND_TRANSFERHANDLER = "transferhandler" + KIND_VALUESET = "value set" + KIND_PURPOSE = "purposet" ) var REALM = ocmlog.DefineSubRealm("OCM plugin handling", "plugins") diff --git a/api/ocm/plugin/descriptor/descriptor.go b/api/ocm/plugin/descriptor/descriptor.go index 3a371040c6..b6e08d5ed6 100644 --- a/api/ocm/plugin/descriptor/descriptor.go +++ b/api/ocm/plugin/descriptor/descriptor.go @@ -24,6 +24,7 @@ type Descriptor struct { LabelMergeSpecifications List[LabelMergeSpecification] `json:"labelMergeSpecifications,omitempty"` ValueSets List[ValueSetDescriptor] `json:"valuesets,omitempty"` Commands List[CommandDescriptor] `json:"commands,omitempty"` + TransferHandlers List[TransferHandlerDescriptor] `json:"transferHandlers,omitempty"` ConfigTypes List[ConfigTypeDescriptor] `json:"configTypes,omitempty"` } @@ -58,6 +59,9 @@ func (d *Descriptor) Capabilities() []string { if len(d.ConfigTypes) > 0 { caps = append(caps, "Config Types") } + if len(d.TransferHandlers) > 0 { + caps = append(caps, "Transfer Handlers") + } return caps } @@ -235,6 +239,40 @@ func (a CommandDescriptor) GetDescription() string { //////////////////////////////////////////////////////////////////////////////// +type TransferHandlerDescriptor struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Questions []QuestionDescriptor `json:"questions,omitempty"` +} + +func (d TransferHandlerDescriptor) GetName() string { + return d.Name +} + +func (d TransferHandlerDescriptor) GetQuestion(name string) *QuestionDescriptor { + for _, q := range d.Questions { + if q.Question == name { + return &q + } + } + return nil +} + +type QuestionDescriptor struct { + // Question is the name of the question the plugin can answer. + // Possible types and their meaning is described by the + // variable common.TransferHandlerQuestions. + Question string `json:"question"` + Description string `json:"description,omitempty"` + // Labels described the list of labels passed to the + // plugin if they exist. If not specified all labels + // are transferred, if an empty list is specified no + // labels are transferred to the plugin command. + Labels *[]string `json:"labels,omitempty"` +} + +//////////////////////////////////////////////////////////////////////////////// + type ConfigTypeDescriptor = ValueTypeDefinition //////////////////////////////////////////////////////////////////////////////// diff --git a/api/ocm/plugin/interface.go b/api/ocm/plugin/interface.go index 4d4091179e..25a17edda8 100644 --- a/api/ocm/plugin/interface.go +++ b/api/ocm/plugin/interface.go @@ -6,10 +6,11 @@ import ( ) const ( - KIND_PLUGIN = descriptor.KIND_PLUGIN - KIND_UPLOADER = descriptor.KIND_UPLOADER - KIND_ACCESSMETHOD = descriptor.KIND_ACCESSMETHOD - KIND_ACTION = descriptor.KIND_ACTION + KIND_PLUGIN = descriptor.KIND_PLUGIN + KIND_UPLOADER = descriptor.KIND_UPLOADER + KIND_ACCESSMETHOD = descriptor.KIND_ACCESSMETHOD + KIND_ACTION = descriptor.KIND_ACTION + KIND_TRANSFERHANDLER = descriptor.KIND_TRANSFERHANDLER ) var TAG = descriptor.REALM @@ -31,3 +32,31 @@ type ( AccessSpecInfo = internal.AccessSpecInfo UploadTargetSpecInfo = internal.UploadTargetSpecInfo ) + +// +// Transfer handler types and constants +// + +const ( + Q_UPDATE_VERSION = internal.Q_UPDATE_VERSION + Q_OVERWRITE_VERSION = internal.Q_OVERWRITE_VERSION + Q_ENFORCE_TRANSPORT = internal.Q_ENFORCE_TRANSPORT + Q_TRANSFER_VERSION = internal.Q_TRANSFER_VERSION + Q_TRANSFER_RESOURCE = internal.Q_TRANSFER_RESOURCE + Q_TRANSFER_SOURCE = internal.Q_TRANSFER_SOURCE +) + +type ( + SourceComponentVersion = internal.SourceComponentVersion + TargetRepositorySpec = internal.TargetRepositorySpec + TransferOptions = internal.TransferOptions + + Artifact = internal.Artifact + AccessInfo = internal.UniformAccessSpecInfo + QuestionArguments = internal.QuestionArguments + ComponentVersionQuestionArguments = internal.ComponentVersionQuestionArguments + ComponentReferenceQuestionArguments = internal.ComponentReferenceQuestionArguments + ArtifactQuestionArguments = internal.ArtifactQuestionArguments + Resolution = internal.Resolution + DecisionRequestResult = internal.DecisionRequestResult +) diff --git a/api/ocm/plugin/internal/access.go b/api/ocm/plugin/internal/access.go index a4238a6533..6bae43934d 100644 --- a/api/ocm/plugin/internal/access.go +++ b/api/ocm/plugin/internal/access.go @@ -9,4 +9,6 @@ type AccessSpecInfo struct { MediaType string `json:"mediaType"` Hint string `json:"hint"` ConsumerId credentials.ConsumerIdentity `json:"consumerId"` + + Info *UniformAccessSpecInfo `json:"info,omitempty"` } diff --git a/api/ocm/plugin/internal/transfer.go b/api/ocm/plugin/internal/transfer.go new file mode 100644 index 0000000000..29be906de0 --- /dev/null +++ b/api/ocm/plugin/internal/transfer.go @@ -0,0 +1,148 @@ +package internal + +import ( + "encoding/json" + "reflect" + + "github.com/mandelsoft/goutils/generics" + + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + v2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + ocm "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/tech" +) + +const ( + Q_UPDATE_VERSION = "updateversion" + Q_OVERWRITE_VERSION = "overwriteversion" + Q_ENFORCE_TRANSPORT = "enforcetransport" + Q_TRANSFER_VERSION = "transferversion" + Q_TRANSFER_RESOURCE = "transferresource" + Q_TRANSFER_SOURCE = "transfersource" +) + +var TransferHandlerQuestions = map[string]reflect.Type{ + Q_UPDATE_VERSION: generics.TypeOf[ComponentVersionQuestionArguments](), + Q_ENFORCE_TRANSPORT: generics.TypeOf[ComponentVersionQuestionArguments](), + Q_OVERWRITE_VERSION: generics.TypeOf[ComponentVersionQuestionArguments](), + Q_TRANSFER_VERSION: generics.TypeOf[ComponentReferenceQuestionArguments](), + Q_TRANSFER_RESOURCE: generics.TypeOf[ArtifactQuestionArguments](), + Q_TRANSFER_SOURCE: generics.TypeOf[ArtifactQuestionArguments](), +} + +type UniformAccessSpecInfo = tech.UniformAccessSpecInfo + +type SourceComponentVersion struct { + Name string `json:"component"` + Version string `json:"version"` + Provider metav1.Provider `json:"provider,omitempty"` + Repository ocm.GenericRepositorySpec `json:"repository"` + Labels metav1.Labels `json:"labels,omitempty"` +} + +type TargetRepositorySpec = ocm.GenericRepositorySpec + +// TransferOptions are the standard transfer options from +// the standard transfer handler. +// Like other transfer handler types it is possible to define +// more options. Those non-standard options are passed +// via a json.RawMessage in the field special. +type TransferOptions struct { + Recursive *bool `json:"recursive,omitempty"` + ResourcesByValue *bool `json:"resourcesByValue,omitempty"` + LocalByValue *bool `json:"localByValue,omitempty"` + SourcesByValue *bool `json:"sourcesByValue,omitempty"` + KeepGlobalAccess *bool `json:"keepGlobalAccess,omitempty"` + StopOnExisting *bool `json:"stopOnExisting,omitempty"` + EnforceTransport *bool `json:"enforceTransport,omitempty"` + Overwrite *bool `json:"overwrite,omitempty"` + SkipUpdate *bool `json:"skipUpdate,omitempty"` + OmitAccessTypes []string `json:"omitAccessTypes,omitempty"` + OmitArtifactTypes []string `json:"omitArtifactTypes,omitempty"` + Special *json.RawMessage `json:"special,omitempty"` +} + +// Resolution describes the transport context for a component +// version, including the new handler specification and +// the source repository to use to look up the reference. +type Resolution struct { + RepositorySpec *ocm.GenericRepositorySpec `json:"repository,omitempty"` + // TransferHandler is the handler identity according to the transfer handler + // name scheme. + TransferHandler string `json:"transferHandler,omitempty"` + // TransferOptions may describe modified options used for sub-sequent + // transfers. + TransferOptions *TransferOptions `json:"transferOptions,omitempty"` +} + +// DecisionRequestResult is the structure of the answer +// the plugin has to return for a question. +type DecisionRequestResult struct { + Error string `json:"error,omitempty"` + Decision bool `json:"decision"` + Resolution *Resolution `json:"resolution,omitempty"` +} + +// QuestionArguments is the interface for the question attributes +// differing for the various questions types. +// There are three basic attribute sets: +// - ComponentVersionQuestionArguments +// - ComponentReferenceQuestionArguments +// - ArtifactQuestionArguments +// +// For type assignments see TransferHandlerQuestions. +type QuestionArguments interface { + QuestionArgumentsType() string +} + +// ComponentVersionQuestionArguments describes the question arguments +// given for a component version related question. +type ComponentVersionQuestionArguments struct { + Source SourceComponentVersion `json:"source"` + Target TargetRepositorySpec `json:"target"` + Options TransferOptions `json:"options"` +} + +var _ QuestionArguments = (*ComponentVersionQuestionArguments)(nil) + +func (a *ComponentVersionQuestionArguments) QuestionArgumentsType() string { + return "ComponentVersionQuestionArguments" +} + +// ComponentReferenceQuestionArguments describes the question arguments +// given for a component version reference related question. +type ComponentReferenceQuestionArguments struct { + Source SourceComponentVersion `json:"source"` + Target TargetRepositorySpec `json:"target"` + + v2.ElementMeta `json:",inline"` + ComponentName string `json:"componentName"` + + Options TransferOptions `json:"options"` +} + +var _ QuestionArguments = (*ComponentReferenceQuestionArguments)(nil) + +func (a *ComponentReferenceQuestionArguments) QuestionArgumentsType() string { + return "ComponentReferenceQuestionArguments" +} + +type Artifact struct { + Meta v2.ElementMeta `json:"metadata"` + Access ocm.GenericAccessSpec `json:"access"` + AccessInfo UniformAccessSpecInfo `json:"accessInfo"` +} + +// ArtifactQuestionArguments describes the question arguments +// given for an artifact related question. +type ArtifactQuestionArguments struct { + Source SourceComponentVersion `json:"source"` + Artifact Artifact `json:"artifact"` + Options TransferOptions `json:"options"` +} + +var _ QuestionArguments = (*ArtifactQuestionArguments)(nil) + +func (a *ArtifactQuestionArguments) QuestionArgumentsType() string { + return "ArtifactQuestionArguments" +} diff --git a/api/ocm/plugin/plugin.go b/api/ocm/plugin/plugin.go index f546a0781a..8f160a210b 100644 --- a/api/ocm/plugin/plugin.go +++ b/api/ocm/plugin/plugin.go @@ -18,6 +18,7 @@ import ( "ocm.software/ocm/api/datacontext/attrs/clicfgattr" "ocm.software/ocm/api/ocm" "ocm.software/ocm/api/ocm/plugin/cache" + "ocm.software/ocm/api/ocm/plugin/internal" "ocm.software/ocm/api/ocm/plugin/ppi" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod/compose" @@ -29,6 +30,7 @@ import ( "ocm.software/ocm/api/ocm/plugin/ppi/cmds/download" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/mergehandler" merge "ocm.software/ocm/api/ocm/plugin/ppi/cmds/mergehandler/execute" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/transferhandler" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload/put" uplval "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload/validate" @@ -443,3 +445,25 @@ func (p *pluginImpl) Command(name string, reader io.Reader, writer io.Writer, cm _, err := p.Exec(reader, writer, args...) return err } + +func (p *pluginImpl) AskTransferQuestion(name string, question string, args interface{}) (*DecisionRequestResult, error) { + argdata, err := json.Marshal(args) + if err != nil { + return nil, errors.Wrapf(err, "cannot marshal option values") + } + + result, err := p.Exec(bytes.NewReader(argdata), nil, transferhandler.Name, name, question) + if err != nil { + return nil, err + } + var r internal.DecisionRequestResult + err = json.Unmarshal(result, &r) + if err != nil { + return nil, errors.Wrapf(err, "cannot unmarshal composition result") + } + + if r.Error == "" { + return &r, nil + } + return &r, errors.New(r.Error) +} diff --git a/api/ocm/plugin/plugin_test.go b/api/ocm/plugin/plugin_test.go index 870e2bdcd9..3968008f34 100644 --- a/api/ocm/plugin/plugin_test.go +++ b/api/ocm/plugin/plugin_test.go @@ -82,7 +82,7 @@ someattr: value Expect(err).To(Succeed()) spec := s.(*access.AccessSpec) h := spec.Handler() - info, err := h.Info(spec) + info, err := h.GetAccessSpecInfo(spec) Expect(err).To(Succeed()) Expect(info).To(Equal(&plugin.AccessSpecInfo{ Short: "a test", diff --git a/api/ocm/plugin/ppi/cmds/app.go b/api/ocm/plugin/ppi/cmds/app.go index b2129bfef4..0f1c117565 100644 --- a/api/ocm/plugin/ppi/cmds/app.go +++ b/api/ocm/plugin/ppi/cmds/app.go @@ -4,6 +4,7 @@ package cmds import ( "encoding/json" + "io" "os" "github.com/spf13/cobra" @@ -17,6 +18,7 @@ import ( "ocm.software/ocm/api/ocm/plugin/ppi/cmds/info" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/mergehandler" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/topics/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/transferhandler" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset" "ocm.software/ocm/api/utils/cobrautils" @@ -31,7 +33,7 @@ func (p *PluginCommand) Command() *cobra.Command { return p.command } -func NewPluginCommand(p ppi.Plugin) *PluginCommand { +func NewPluginCommand(p ppi.Plugin, opts ...Option) *PluginCommand { short := p.Descriptor().Short if short == "" { short = "OCM plugin " + p.Name() @@ -66,6 +68,7 @@ func NewPluginCommand(p ppi.Plugin) *PluginCommand { cmd.AddCommand(download.New(p)) cmd.AddCommand(valueset.New(p)) cmd.AddCommand(command.New(p)) + cmd.AddCommand(transferhandler.New(p)) cmd.InitDefaultHelpCmd() help := cobrautils.GetHelpCommand(cmd) @@ -76,8 +79,12 @@ func NewPluginCommand(p ppi.Plugin) *PluginCommand { help.AddCommand(descriptor.New()) - p.GetOptions().AddFlags(cmd.Flags()) pcmd.command = cmd + for _, o := range opts { + o.ApplyTo(pcmd) + } + + p.GetOptions().AddFlags(cmd.Flags()) return pcmd } @@ -92,6 +99,18 @@ func (p *PluginCommand) PreRunE(cmd *cobra.Command, args []string) error { return nil } +func (p *PluginCommand) SetIO(in io.Reader, out, err io.Writer) { + if in != nil { + p.command.SetIn(in) + } + if out != nil { + p.command.SetOut(out) + } + if err != nil { + p.command.SetErr(err) + } +} + func (p *PluginCommand) Execute(args []string) error { p.command.SetArgs(args) err := p.command.Execute() diff --git a/api/ocm/plugin/ppi/cmds/descopts.go b/api/ocm/plugin/ppi/cmds/descopts.go new file mode 100644 index 0000000000..29b8f56ac8 --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/descopts.go @@ -0,0 +1,49 @@ +package cmds + +import ( + "io" + + "github.com/mandelsoft/goutils/optionutils" +) + +type Option = optionutils.Option[*PluginCommand] + +type stdout struct { + io.Writer +} + +func StdOut(w io.Writer) Option { + return &stdout{w} +} + +func (o *stdout) ApplyTo(p *PluginCommand) { + p.command.SetOut(o.Writer) +} + +//////////////////////////////////////////////////////////////////////////////// + +type stderr struct { + io.Writer +} + +func StdErr(w io.Writer) Option { + return &stderr{w} +} + +func (o *stderr) ApplyTo(p *PluginCommand) { + p.command.SetErr(o.Writer) +} + +//////////////////////////////////////////////////////////////////////////////// + +type stdin struct { + io.Reader +} + +func StdIn(r io.Reader) Option { + return &stdin{r} +} + +func (o *stdin) ApplyTo(p *PluginCommand) { + p.command.SetIn(o.Reader) +} diff --git a/api/ocm/plugin/ppi/cmds/transferhandler/cmd.go b/api/ocm/plugin/ppi/cmds/transferhandler/cmd.go new file mode 100644 index 0000000000..ef00ee996c --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/transferhandler/cmd.go @@ -0,0 +1,117 @@ +package transferhandler + +import ( + "encoding/json" + "fmt" + "io" + "reflect" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/ocm/plugin/ppi" +) + +const Name = "transferhandler" + +func New(p ppi.Plugin) *cobra.Command { + opts := Options{} + cmd := &cobra.Command{ + Use: Name + " ", + Short: "decide on a question related to a component version transport", + Long: ` +The task of this command is to decide on questions related to the transport +of component versions, their resources and sources. + +This command is only used for actions for which the transfer handler descriptor +enables the particular question. + +The question arguments are passed as JSON structure on *stdin*, +the result has to be returned as JSON document +on *stdout*. + +There are several questions a handler can answer: +- transferversion: This action answers the question, whether + a component version shall be transported at all and how it should be + transported. The argument is a ComponentVersionQuestion + and the result decision is extended by an optional transport context + consisting, of a new transport handler description and transport options. +- enforcetransport: This action answers the question, whether + a component version shall be transported as if it is not yet present + in the target repository. The argument is a ComponentVersionQuestion. +- updateversion: Update non-signature relevant information. + The argument is a ComponentVersionQuestion. +- overwriteversion: Override signature-relevant information. + The argument is a ComponentVersionQuestion. +- transferresource: Transport resource as value. The argument + is an ArtifactQuestion. +- transfersource: Transport source as value. The argument + is an ArtifactQuestion. + +For detailed types see ocm.software/ocm/api/ocm/plugin/ppi/questions.go. +`, + Args: cobra.ExactArgs(2), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return opts.Complete(args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return Command(p, cmd, &opts) + }, + } + opts.AddFlags(cmd.Flags()) + return cmd +} + +type Options struct { + // Handler is the handler name as provided by the plugin. + Handler string + // Question cis the name of the question + Question string +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { +} + +func (o *Options) Complete(args []string) error { + o.Handler = args[0] + o.Question = args[1] + return nil +} + +func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { + t := ppi.TransferHandlerQuestions[opts.Question] + if t == nil { + return fmt.Errorf("unknown question %q", opts.Question) + } + h := p.GetTransferHandler(opts.Handler) + if h == nil { + return fmt.Errorf("unknown transfer handler %q", opts.Handler) + } + for _, q := range h.GetQuestions() { + if q.GetQuestion() == opts.Question { + data, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + + args := reflect.New(t).Interface().(ppi.QuestionArguments) + err = json.Unmarshal(data, args) + if err != nil { + return err + } + + var result ppi.DecisionRequestResult + result.Decision, err = q.DecideOn(p, args) + if err != nil { + result.Error = err.Error() + } + data, err = json.Marshal(result) + if err != nil { + return err + } + cmd.Printf("%s\n", string(data)) + return nil + } + } + return fmt.Errorf("question %q not configured for transfer handler %q", opts.Question, opts.Handler) +} diff --git a/api/ocm/plugin/ppi/cmds/valueset/compose/cmd.go b/api/ocm/plugin/ppi/cmds/valueset/compose/cmd.go index 1e5ca775e1..75303d496e 100644 --- a/api/ocm/plugin/ppi/cmds/valueset/compose/cmd.go +++ b/api/ocm/plugin/ppi/cmds/valueset/compose/cmd.go @@ -29,7 +29,10 @@ The finally composed set has to be returned as JSON document on *stdout*. This command is only used, if for a value set descriptor configuration -na direct composition rules are configured (` + p.Name() + ` descriptor). +no direct composition rules are configured (` + p.Name() + ` descriptor). + +The purpose describes the purpose the values set is used for: +- routingslip a value used for a routing slip entry. If possible, predefined standard options should be used. In such a case only the name field should be defined for an option. If required, new options can be diff --git a/api/ocm/plugin/ppi/interface.go b/api/ocm/plugin/ppi/interface.go index bb35c71b04..752971ce30 100644 --- a/api/ocm/plugin/ppi/interface.go +++ b/api/ocm/plugin/ppi/interface.go @@ -28,6 +28,10 @@ type ( AccessSpecInfo = internal.AccessSpecInfo ValueSetInfo = internal.ValueSetInfo UploadTargetSpecInfo = internal.UploadTargetSpecInfo + + SourceComponentVersion = internal.SourceComponentVersion + TargetRepositorySpec = internal.TargetRepositorySpec + StandardTransferOptions = internal.TransferOptions ) var REALM = descriptor.REALM @@ -72,6 +76,10 @@ type Plugin interface { GetCommand(name string) Command Commands() []Command + RegisterTransferHandler(h TransferHandler) error + GetTransferHandler(name string) TransferHandler + TransferHandlers() []TransferHandler + RegisterConfigType(c cpi.ConfigType) error GetConfigType(name string) *descriptor.ConfigTypeDescriptor ConfigTypes() []descriptor.ConfigTypeDescriptor @@ -211,3 +219,50 @@ type Command interface { Command() *cobra.Command } + +// TransferHandler is the support interface +// for implementing a transfer handler for the plugin support +// library. +// There is a standard implementation NewTransferHandler. +type TransferHandler interface { + GetName() string + GetDescription() string + GetQuestions() []DecisionHandler +} + +// DecisionHandler is the support interface for implementing +// the answer to a question used for the TransferHandler. +// A base implementation providing the non-functional attributues +// cane be obtained by NewDecisionHandlerBase. +type DecisionHandler interface { + // GetQuestion returns the name of the question answered by this handler + // (see common.TransferHandlerQuestions). + GetQuestion() string + + GetDescription() string + // GetLabels returns the list of labels, which should be passed + // to the transfer handler. If nothing is specified all labels + // are transferred, if an empty list is given no label is handed over + // to the plugin command. + GetLabels() *[]string + + // DecideOn implements the calculation of the answer to + // the question. The given question contains the arguments for + // the questions. There are three kinds of arguments: + // ArtifactQuestionArguments, ComponentVersionQuestionArguments and ComponentReferenceQuestionArguments. + // TransferHandlerQuestions maps the question name to the used + // argument type. + DecideOn(p Plugin, question QuestionArguments) (bool, error) +} + +type ( + TransferOptions = internal.TransferOptions + Artifact = internal.Artifact + AccessInfo = internal.UniformAccessSpecInfo + QuestionArguments = internal.QuestionArguments + ComponentVersionQuestionArguments = internal.ComponentVersionQuestionArguments + ComponentReferenceQuestionArguments = internal.ComponentReferenceQuestionArguments + ArtifactQuestionArguments = internal.ArtifactQuestionArguments + Resolution = internal.Resolution + DecisionRequestResult = internal.DecisionRequestResult +) diff --git a/api/ocm/plugin/ppi/plugin.go b/api/ocm/plugin/ppi/plugin.go index 319a2891ea..03e5b46406 100644 --- a/api/ocm/plugin/ppi/plugin.go +++ b/api/ocm/plugin/ppi/plugin.go @@ -46,6 +46,8 @@ type plugin struct { valuesets map[string]map[string]ValueSet setScheme map[string]runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]] + transferhandlers map[string]TransferHandler + clicmds map[string]Command configParser func(message json.RawMessage) (interface{}, error) @@ -70,6 +72,8 @@ func NewPlugin(name string, version string) Plugin { mergehandlers: map[string]ValueMergeHandler{}, mergespecs: map[string]*descriptor.LabelMergeSpecification{}, + transferhandlers: map[string]TransferHandler{}, + valuesets: map[string]map[string]ValueSet{}, setScheme: map[string]runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]]{}, @@ -597,6 +601,37 @@ func (p *plugin) Commands() []Command { //////////////////////////////////////////////////////////////////////////////// +func (p *plugin) GetTransferHandler(name string) TransferHandler { + return p.transferhandlers[name] +} + +func (p *plugin) RegisterTransferHandler(h TransferHandler) error { + if p.GetTransferHandler(h.GetName()) != nil { + return errors.ErrAlreadyExists("transfer handler", h.GetName()) + } + d := descriptor.TransferHandlerDescriptor{ + Name: h.GetName(), + Description: h.GetDescription(), + } + for _, q := range h.GetQuestions() { + qd := descriptor.QuestionDescriptor{ + Question: q.GetQuestion(), + Description: q.GetDescription(), + Labels: q.GetLabels(), + } + d.Questions = append(d.Questions, qd) + } + p.descriptor.TransferHandlers = append(p.descriptor.TransferHandlers, d) + p.transferhandlers[h.GetName()] = h + return nil +} + +func (p *plugin) TransferHandlers() []TransferHandler { + return maputils.OrderedValues(p.transferhandlers) +} + +//////////////////////////////////////////////////////////////////////////////// + func (p *plugin) GetConfigType(name string) *descriptor.ConfigTypeDescriptor { var def *descriptor.ConfigTypeDescriptor for _, d := range p.descriptor.ConfigTypes { diff --git a/api/ocm/plugin/ppi/questions.go b/api/ocm/plugin/ppi/questions.go new file mode 100644 index 0000000000..368c47477b --- /dev/null +++ b/api/ocm/plugin/ppi/questions.go @@ -0,0 +1,16 @@ +package ppi + +import ( + "ocm.software/ocm/api/ocm/plugin/internal" +) + +const ( + Q_UPDATE_VERSION = internal.Q_UPDATE_VERSION + Q_OVERWRITE_VERSION = internal.Q_OVERWRITE_VERSION + Q_ENFORCE_TRANSPORT = internal.Q_ENFORCE_TRANSPORT + Q_TRANSFER_VERSION = internal.Q_TRANSFER_VERSION + Q_TRANSFER_RESOURCE = internal.Q_TRANSFER_RESOURCE + Q_TRANSFER_SOURCE = internal.Q_TRANSFER_SOURCE +) + +var TransferHandlerQuestions = internal.TransferHandlerQuestions diff --git a/api/ocm/plugin/ppi/utils.go b/api/ocm/plugin/ppi/utils.go index 27b0885d73..f4fd3068ab 100644 --- a/api/ocm/plugin/ppi/utils.go +++ b/api/ocm/plugin/ppi/utils.go @@ -4,7 +4,8 @@ import ( "encoding/json" "reflect" - "github.com/pkg/errors" + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/generics" "golang.org/x/exp/slices" "ocm.software/ocm/api/ocm/extensions/accessmethods/options" @@ -13,6 +14,8 @@ import ( type decoder runtime.TypedObjectDecoder[runtime.TypedObject] +const KIND_QUESTION = "question" + type AccessMethodBase struct { decoder nameDescription @@ -115,6 +118,144 @@ func (b *nameDescription) Description() string { //////////////////////////////////////////////////////////////////////////////// +type transferHandler struct { + name string + description string + questions []DecisionHandler +} + +func NewTransferHandler(name, desc string) *transferHandler { + return &transferHandler{ + name: name, + description: desc, + questions: nil, + } +} + +func (t *transferHandler) GetName() string { + return t.name +} + +func (t *transferHandler) GetDescription() string { + return t.description +} + +func (t *transferHandler) GetQuestions() []DecisionHandler { + return t.questions +} + +func (t *transferHandler) RegisterDecision(h DecisionHandler) error { + if TransferHandlerQuestions[h.GetQuestion()] == nil { + return errors.ErrInvalid(KIND_QUESTION, h.GetQuestion()) + } + for _, e := range t.questions { + if e.GetQuestion() == h.GetQuestion() { + return errors.ErrAlreadyExists(KIND_QUESTION, e.GetQuestion()) + } + } + t.questions = append(t.questions, h) + return nil +} + +// DecisionHandlerBase provides access to the +// non-functional attributes of a DecisionHandler. +// It can be created with NewDecisionHandlerBase and +// embedded into the final DecisionHandler implementation. +type DecisionHandlerBase struct { + question string + description string + labels *[]string +} + +func (d *DecisionHandlerBase) GetQuestion() string { + return d.question +} + +func (d *DecisionHandlerBase) GetDescription() string { + return d.description +} + +func (d *DecisionHandlerBase) GetLabels() *[]string { + return d.labels +} + +func NewDecisionHandlerBase(q, desc string, labels ...string) DecisionHandlerBase { + return DecisionHandlerBase{q, desc, generics.Pointer(slices.Clone(labels))} +} + +//////////////////////////////////////////////////////////////////////////////// + +type QuestionResultFunc func(p Plugin, question QuestionArguments) (bool, error) + +func ComponentVersionQuestionFunc(f func(p Plugin, question *ComponentVersionQuestionArguments) (bool, error)) QuestionResultFunc { + return func(p Plugin, question QuestionArguments) (bool, error) { + return f(p, question.(*ComponentVersionQuestionArguments)) + } +} + +type ComponentReferenceQuestionFunc = func(p Plugin, question *ComponentReferenceQuestionArguments) (bool, error) + +func ForComponentReferenceQuestion(f func(p Plugin, question *ComponentReferenceQuestionArguments) (bool, error)) QuestionResultFunc { + return func(p Plugin, question QuestionArguments) (bool, error) { + return f(p, question.(*ComponentReferenceQuestionArguments)) + } +} + +type ArtifactQuestionFunc = func(p Plugin, question *ArtifactQuestionArguments) (bool, error) + +func ForArtifactQuestion(f ArtifactQuestionFunc) QuestionResultFunc { + return func(p Plugin, question QuestionArguments) (bool, error) { + return f(p, question.(*ArtifactQuestionArguments)) + } +} + +type defaultDecisionHandler struct { + DecisionHandlerBase + handler func(p Plugin, question QuestionArguments) (bool, error) +} + +// NewDecisionHandler provides a default decision handler based on its standard +// fields and a handler function. +func NewDecisionHandler(q, desc string, h QuestionResultFunc, labels ...string) DecisionHandler { + return &defaultDecisionHandler{ + DecisionHandlerBase: NewDecisionHandlerBase(q, desc, labels...), + handler: h, + } +} + +func (d *defaultDecisionHandler) DecideOn(p Plugin, question QuestionArguments) (bool, error) { + return d.handler(p, question) +} + +//////////////////////////////////////////////////////////////////////////////// +// specialized handler creation + +func NewTransferResourceDecision(desc string, h ArtifactQuestionFunc, labels ...string) DecisionHandler { + return NewDecisionHandler(Q_TRANSFER_RESOURCE, desc, ForArtifactQuestion(h)) +} + +func NewTransferSourceDecision(desc string, h ArtifactQuestionFunc, labels ...string) DecisionHandler { + return NewDecisionHandler(Q_TRANSFER_SOURCE, desc, ForArtifactQuestion(h)) +} + +func NewEnforceTransportDesision(desc string, h ComponentReferenceQuestionFunc, labels ...string) DecisionHandler { + return NewDecisionHandler(Q_ENFORCE_TRANSPORT, desc, ForComponentReferenceQuestion(h)) +} + +func NewTransferVersionDecision(desc string, h ComponentReferenceQuestionFunc, labels ...string) DecisionHandler { + return NewDecisionHandler(Q_TRANSFER_VERSION, desc, ForComponentReferenceQuestion(h)) +} + +func NewOverwriteVersionDecision(desc string, h ComponentReferenceQuestionFunc, labels ...string) DecisionHandler { + return NewDecisionHandler(Q_OVERWRITE_VERSION, desc, ForComponentReferenceQuestion(h)) +} + +func NewUpdateVersionDecision(desc string, h ComponentReferenceQuestionFunc, labels ...string) DecisionHandler { + return NewDecisionHandler(Q_UPDATE_VERSION, desc, ForComponentReferenceQuestion(h)) +} + +//////////////////////////////////////////////////////////////////////////////// + // Config is a generic structured config stored in a string map. type Config map[string]interface{} diff --git a/api/ocm/ref.go b/api/ocm/ref.go index 72f0b08e84..4f73eef47a 100644 --- a/api/ocm/ref.go +++ b/api/ocm/ref.go @@ -145,14 +145,6 @@ func (r *RefSpec) Name() string { return fmt.Sprintf("%s/%s//%s", r.Host, r.SubPath, r.Component) } -func (r *RefSpec) HostPort() (string, string) { - i := strings.Index(r.Host, ":") - if i < 0 { - return r.Host, "" - } - return r.Host[:i], r.Host[i+1:] -} - func (r *RefSpec) Reference() string { t := r.Type if t != "" { diff --git a/api/ocm/tools/transfer/init.go b/api/ocm/tools/transfer/init.go index a68ad2ce93..26d7b5e1a3 100644 --- a/api/ocm/tools/transfer/init.go +++ b/api/ocm/tools/transfer/init.go @@ -2,6 +2,7 @@ package transfer import ( _ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/config" + _ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/plugin" _ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/spiff" _ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" ) diff --git a/api/ocm/tools/transfer/options.go b/api/ocm/tools/transfer/options.go index 4c9e66dcf2..cd1d688ca6 100644 --- a/api/ocm/tools/transfer/options.go +++ b/api/ocm/tools/transfer/options.go @@ -41,6 +41,9 @@ type localOptions struct { func (opts *localOptions) Eval(optlist ...transferhandler.TransferOption) error { for _, o := range optlist { + if o == nil { + continue + } if _, ok := o.(transferhandler.TransferOptionsCreator); !ok { err := o.ApplyTransferOption(opts) if err != nil { diff --git a/api/ocm/tools/transfer/transferhandler/options.go b/api/ocm/tools/transfer/transferhandler/options.go new file mode 100644 index 0000000000..cf290daaa8 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/options.go @@ -0,0 +1,39 @@ +package transferhandler + +import ( + "github.com/mandelsoft/goutils/errors" +) + +////////////////////////////////////////////////////////////////////////////// + +// ConfigOptionConsumer is an interface for option sets supporting generic +// non-standard options. +// Specialized option set implementations map such generic +// config to their specialized settings. The format depends +// on the option target. For example, for spiff it is a spiff +// script. +type ConfigOptionConsumer interface { + SetConfig([]byte) + GetConfig() []byte +} + +type configOption struct { + config []byte +} + +func (o *configOption) ApplyTransferOption(to TransferOptions) error { + if eff, ok := to.(ConfigOptionConsumer); ok { + eff.SetConfig(o.config) + return nil + } else { + return errors.ErrNotSupported(KIND_TRANSFEROPTION, "config") + } +} + +// WithConfig configures a handler specific configuration. +// It is accepted by all handlers featuring such a config possibility. +func WithConfig(config []byte) TransferOption { + return &configOption{ + config: config, + } +} diff --git a/api/ocm/tools/transfer/transferhandler/plugin/handler.go b/api/ocm/tools/transfer/transferhandler/plugin/handler.go new file mode 100644 index 0000000000..74116faf69 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/handler.go @@ -0,0 +1,408 @@ +package plugin + +import ( + "encoding/json" + "sync" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/sliceutils" + "golang.org/x/exp/slices" + + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + v2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/resolvers" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" + "ocm.software/ocm/api/utils/refmgmt" + "ocm.software/ocm/api/utils/runtime" +) + +type Handler struct { + lock sync.Mutex + standard.Handler + opts *Options + plugin plugin.Plugin + desc *descriptor.TransferHandlerDescriptor +} + +func New(opts ...transferhandler.TransferOption) (transferhandler.TransferHandler, error) { + options := &Options{} + err := transferhandler.ApplyOptions(options, opts...) + if err != nil { + return nil, err + } + + return &Handler{ + Handler: *standard.NewDefaultHandler(&options.Options), + opts: options, + }, nil +} + +func (h *Handler) GetConfig() []byte { + return h.opts.GetConfig() +} + +func (h *Handler) ResolvePlugin(ctx ocm.ContextProvider) (plugin.Plugin, error) { + h.lock.Lock() + defer h.lock.Unlock() + + if h.plugin == nil { + pi := plugincacheattr.Get(ctx) + if pi == nil { + return nil, errors.ErrUnknown(plugin.KIND_PLUGIN, h.opts.plugin) + } + p := pi.Get(h.opts.plugin) + if p == nil { + return nil, errors.ErrUnknown(plugin.KIND_PLUGIN, h.opts.plugin) + } + h.plugin = p + h.desc = p.GetDescriptor().TransferHandlers.Get(h.opts.GetTransferHandler()) + if h.desc == nil { + return nil, errors.ErrUnknown(plugin.KIND_TRANSFERHANDLER, h.opts.plugin) + } + } + return h.plugin, nil +} + +func filterLabels(in metav1.Labels, filter *[]string) metav1.Labels { + var r metav1.Labels + + if filter == nil { + return in.Copy() + } + if len(*filter) > 0 { + for _, l := range in { + if filter == nil || slices.Contains(*filter, l.Name) { + r = append(r, l) + } + } + } + return r +} + +func (h *Handler) transferOptions() (*plugin.TransferOptions, error) { + var special *json.RawMessage + + if h.opts.config != nil { + s, err := runtime.ToJSON(h.opts.config) + if err != nil { + return nil, err + } + special = &s + } + return &plugin.TransferOptions{ + Recursive: h.opts.GetRecursive(), + ResourcesByValue: h.opts.GetResourcesByValue(), + LocalByValue: h.opts.GetLocalResourcesByValue(), + SourcesByValue: h.opts.GetSourcesByValue(), + KeepGlobalAccess: h.opts.GetKeepGlobalAccess(), + StopOnExisting: h.opts.GetStopOnExistingVersion(), + EnforceTransport: h.opts.GetEnforceTransport(), + Overwrite: h.opts.GetOverwrite(), + SkipUpdate: h.opts.GetSkipUpdate(), + OmitAccessTypes: h.opts.GetOmittedAccessTypes(), + OmitArtifactTypes: h.opts.GetOmittedArtifactTypes(), + Special: special, + }, nil +} + +func (h *Handler) sourceComponentVersion(src ocm.ComponentVersionAccess, question string) (*plugin.SourceComponentVersion, error) { + repo := src.Repository() + defer repo.Close() + + repospec, err := ocm.ToGenericRepositorySpec(repo.GetSpecification()) + if err != nil { + return nil, err + } + + filter := h.desc.GetQuestion(question).Labels + + return &plugin.SourceComponentVersion{ + Name: src.GetName(), + Version: src.GetVersion(), + Provider: metav1.Provider{ + Name: src.GetProvider().GetName(), + Labels: filterLabels(src.GetProvider().Labels, filter), + }, + Repository: *repospec, + Labels: filterLabels(src.GetDescriptor().Labels, filter), + }, nil +} + +func (h *Handler) respositoryTarget(tgt ocm.ComponentVersionAccess) (*plugin.TargetRepositorySpec, error) { + repo := tgt.Repository() + defer repo.Close() + + return ocm.ToGenericRepositorySpec(repo.GetSpecification()) +} + +func (h *Handler) artifact(ctx ocm.Context, art ocm.AccessSpec, ra *compdesc.ElementMeta, question string) (*plugin.Artifact, error) { + filter := h.desc.GetQuestion(question).Labels + + a := art.Info(ctx) + s, err := ocm.ToGenericAccessSpec(art) + if err != nil { + return nil, err + } + + return &plugin.Artifact{ + Meta: v2.ElementMeta{ + Name: ra.Name, + Version: ra.Version, + ExtraIdentity: ra.ExtraIdentity.Copy(), + Labels: filterLabels(ra.Labels, filter), + }, + Access: *s, + AccessInfo: *a, + }, nil +} + +func (h *Handler) askComponentVersionQuestion(question string, src ocm.ComponentVersionAccess, tgt ocm.ComponentVersionAccess) (bool, error) { + p, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return false, err + } + + s, err := h.sourceComponentVersion(src, question) + if err != nil { + return false, err + } + t, err := h.respositoryTarget(tgt) + if err != nil { + return false, err + } + o, err := h.transferOptions() + if err != nil { + return false, err + } + + args := plugin.ComponentVersionQuestionArguments{ + Source: *s, + Target: *t, + Options: *o, + } + + r, err := p.AskTransferQuestion(h.opts.handler, question, args) + if err != nil { + return false, err + } + return r.Decision, nil +} + +func (h *Handler) askArtifactQuestion(question string, src ocm.ComponentVersionAccess, art ocm.AccessSpec, ra *compdesc.ElementMeta) (bool, error) { + p, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return false, err + } + + s, err := h.sourceComponentVersion(src, question) + if err != nil { + return false, err + } + a, err := h.artifact(src.GetContext(), art, ra, question) + if err != nil { + return false, err + } + o, err := h.transferOptions() + if err != nil { + return false, err + } + + args := plugin.ArtifactQuestionArguments{ + Source: *s, + Artifact: *a, + Options: *o, + } + + r, err := p.AskTransferQuestion(h.opts.handler, question, args) + if err != nil { + return false, err + } + return r.Decision, nil +} + +func (h *Handler) UpdateVersion(src ocm.ComponentVersionAccess, tgt ocm.ComponentVersionAccess) (bool, error) { + _, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return false, err + } + if h.desc.GetQuestion(plugin.Q_UPDATE_VERSION) == nil { + return h.Handler.UpdateVersion(src, tgt) + } + return h.askComponentVersionQuestion(plugin.Q_UPDATE_VERSION, src, tgt) +} + +func (h *Handler) EnforceTransport(src ocm.ComponentVersionAccess, tgt ocm.ComponentVersionAccess) (bool, error) { + _, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return false, err + } + if h.desc.GetQuestion(plugin.Q_ENFORCE_TRANSPORT) == nil { + return h.Handler.EnforceTransport(src, tgt) + } + return h.askComponentVersionQuestion(plugin.Q_ENFORCE_TRANSPORT, src, tgt) +} + +func (h *Handler) OverwriteVersion(src ocm.ComponentVersionAccess, tgt ocm.ComponentVersionAccess) (bool, error) { + _, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return false, err + } + if h.desc.GetQuestion(plugin.Q_OVERWRITE_VERSION) == nil { + return h.Handler.OverwriteVersion(src, tgt) + } + return h.askComponentVersionQuestion(plugin.Q_OVERWRITE_VERSION, src, tgt) +} + +func (h *Handler) TransferVersion(repo ocm.Repository, src ocm.ComponentVersionAccess, meta *compdesc.Reference, tgt ocm.Repository) (ocm.ComponentVersionAccess, transferhandler.TransferHandler, error) { + _, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return nil, nil, err + } + if h.desc.GetQuestion(plugin.Q_TRANSFER_VERSION) == nil || src == nil { + return h.Handler.TransferVersion(repo, src, meta, tgt) + } + + filter := h.desc.GetQuestion(plugin.Q_TRANSFER_VERSION).Labels + p, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return nil, nil, err + } + + s, err := h.sourceComponentVersion(src, plugin.Q_TRANSFER_VERSION) + if err != nil { + return nil, nil, err + } + + t, err := ocm.ToGenericRepositorySpec(repo.GetSpecification()) + if err != nil { + return nil, nil, err + } + o, err := h.transferOptions() + if err != nil { + return nil, nil, err + } + + args := plugin.ComponentReferenceQuestionArguments{ + Source: *s, + Target: *t, + ElementMeta: v2.ElementMeta{ + Name: meta.Name, + Version: meta.Version, + ExtraIdentity: meta.ExtraIdentity.Copy(), + Labels: filterLabels(meta.Labels, filter), + }, + Options: *o, + } + + r, err := p.AskTransferQuestion(h.opts.handler, plugin.Q_TRANSFER_VERSION, args) + if err != nil { + return nil, nil, err + } + + // TODO: evaluate transfer handler and repo + + if h.opts.IsStopOnExistingVersion() && tgt != nil { + if found, err := tgt.ExistsComponentVersion(meta.ComponentName, meta.Version); found || err != nil { + return nil, nil, errors.Wrapf(err, "failed looking up in target") + } + } + + if !r.Decision { + return nil, nil, nil + } + + if r.Resolution != nil { + // calculate transfer context for referenced component. + var opts sliceutils.Slice[transferhandler.TransferOption] + + handler := transferhandler.TransferHandler(h) + if r.Resolution.TransferOptions != nil { + opts = transferOptions(r.Resolution.TransferOptions) + handler, err = New(opts...) + if err != nil { + return nil, nil, errors.Wrapf(err, "option mismatch for plugin handler") + } + } else { + opts.Add(h.opts) + } + + if r.Resolution.TransferHandler != "" { + // override handler + handler, err = transferhandler.For(src.GetContext()).ByName(src.GetContext(), r.Resolution.TransferHandler, opts...) + if err != nil { + return nil, nil, errors.Wrapf(err, "invalid transfer handler %q", r.Resolution.TransferHandler) + } + } + + if r.Resolution.RepositorySpec != nil { + repo, err = refmgmt.ToLazy(src.GetContext().RepositoryForSpec(r.Resolution.RepositorySpec)) + if err != nil { + return nil, nil, err + } + defer repo.Close() + } + compoundResolver := resolvers.NewCompoundResolver(repo, h.opts.GetResolver()) + cv, err := compoundResolver.LookupComponentVersion(meta.GetComponentName(), meta.Version) + return cv, handler, err + } + return h.Handler.TransferVersion(repo, src, meta, tgt) +} + +func (h *Handler) TransferResource(src ocm.ComponentVersionAccess, a ocm.AccessSpec, r ocm.ResourceAccess) (bool, error) { + _, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return false, err + } + if h.desc.GetQuestion(plugin.Q_TRANSFER_RESOURCE) == nil { + return h.Handler.TransferResource(src, a, r) + } + return h.askArtifactQuestion(ppi.Q_TRANSFER_RESOURCE, src, a, &r.Meta().ElementMeta) +} + +func (h *Handler) TransferSource(src ocm.ComponentVersionAccess, a ocm.AccessSpec, r ocm.SourceAccess) (bool, error) { + _, err := h.ResolvePlugin(src.GetContext()) + if err != nil { + return false, err + } + if h.desc.GetQuestion(plugin.Q_TRANSFER_SOURCE) == nil { + return h.Handler.TransferSource(src, a, r) + } + return h.askArtifactQuestion(ppi.Q_TRANSFER_SOURCE, src, a, &r.Meta().ElementMeta) +} + +func transferOptions(o *plugin.TransferOptions) []transferhandler.TransferOption { + var opts sliceutils.Slice[transferhandler.TransferOption] + apply(&opts, o.KeepGlobalAccess, standard.KeepGlobalAccess) + apply(&opts, o.ResourcesByValue, standard.ResourcesByValue) + apply(&opts, o.SourcesByValue, standard.SourcesByValue) + apply(&opts, o.EnforceTransport, standard.EnforceTransport) + apply(&opts, o.LocalByValue, standard.LocalResourcesByValue) + apply(&opts, o.Overwrite, standard.Overwrite) + apply(&opts, o.Recursive, standard.Recursive) + apply(&opts, o.StopOnExisting, standard.StopOnExistingVersion) + apply(&opts, o.SkipUpdate, standard.SkipUpdate) + + if o.OmitAccessTypes != nil { + opts.Add(standard.OmitAccessTypes(o.OmitAccessTypes...)) + } + if o.OmitArtifactTypes != nil { + opts.Add(standard.OmitAccessTypes(o.OmitArtifactTypes...)) + } + if o.Special != nil { + opts.Add(transferhandler.WithConfig(*o.Special)) + } + return opts +} + +func apply[T any, O any](opts *sliceutils.Slice[O], o *T, c func(...T) O) { + if o != nil { + opts.Add(c(*o)) + } +} diff --git a/api/ocm/tools/transfer/transferhandler/plugin/handler_test.go b/api/ocm/tools/transfer/transferhandler/plugin/handler_test.go new file mode 100644 index 0000000000..50c7fa84e7 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/handler_test.go @@ -0,0 +1,186 @@ +//go:build unix + +package plugin_test + +import ( + "os" + "reflect" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/api/ocm/plugin/testutils" + . "ocm.software/ocm/cmds/ocm/testhelper" + + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/api/ocm/extensions/repositories/ocireg" + "ocm.software/ocm/api/ocm/internal" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/utils/blobaccess" + + "github.com/mandelsoft/goutils/generics" + + "ocm.software/ocm/api/ocm/tools/transfer" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/plugin" +) + +const PLUGIN = "transferplugin" +const HANDLER = "demo" + +var _ = Describe("Plugin Handler Test Environment", func() { + Context("handler creation", func() { + It("", func() { + f := Must(transfer.NewTransferHandler(plugin.Plugin(PLUGIN), plugin.TransferHandler(HANDLER))) + Expect(reflect.TypeOf(f)).To(Equal(generics.TypeOf[*plugin.Handler]())) + }) + }) + + Context("plugin execution", func() { + var env *TestEnv + var plugins TempPluginDir + + BeforeEach(func() { + env = NewTestEnv(TestData()) + plugins = Must(ConfigureTestPlugins(env, "testdata/plugins")) + }) + + AfterEach(func() { + plugins.Cleanup() + env.Cleanup() + }) + + It("loads plugin", func() { + registry := plugincacheattr.Get(env) + // Expect(registration.RegisterExtensions(env)).To(Succeed()) + p := registry.Get(PLUGIN) + Expect(p).NotTo(BeNil()) + Expect(p.Error()).To(Equal("")) + }) + + It("answers question", func() { + config := Must(os.ReadFile("testdata/config")) + f := Must(transfer.NewTransferHandler(plugin.Plugin(PLUGIN), plugin.TransferHandler(HANDLER), plugin.TransferHandlerConfig(config))) + + cv := FakeCV(env.OCMContext()) + + ra := &ResourceAccess{ + ElementMeta: compdesc.ElementMeta{ + Name: "test", + Version: "1.0.0", + }, + } + b := Must(f.TransferResource(cv, ociartifact.New("ghcr.io/open-component-model/test:v1"), ra)) + Expect(b).To(BeTrue()) + + b = Must(f.TransferResource(cv, ociartifact.New("gcr.io/open-component-model/test:v1"), ra)) + Expect(b).To(BeFalse()) + }) + + Context("registrations", func() { + It("finds plugin handler by name", func() { + h := Must(transferhandler.For(env).ByName(env, "plugin/transferplugin/demo")) + Expect(reflect.TypeOf(h)).To(Equal(generics.TypeOf[*plugin.Handler]())) + }) + + It("finds plugin handler by name and config", func() { + config := Must(os.ReadFile("testdata/config")) + h := Must(transferhandler.For(env).ByName(env, "plugin/transferplugin/demo", transferhandler.WithConfig(config))) + Expect(reflect.TypeOf(h)).To(Equal(generics.TypeOf[*plugin.Handler]())) + Expect(h.(*plugin.Handler).GetConfig()).To(YAMLEqual(config)) + }) + }) + }) +}) + +//////////////////////////////////////////////////////////////////////////////// + +type ComponentVersionAccess struct { + cpi.DummyComponentVersionAccess +} + +func FakeCV(ctx ocm.Context) ocm.ComponentVersionAccess { + return &ComponentVersionAccess{ + cpi.DummyComponentVersionAccess{ctx}, + } +} + +func (a *ComponentVersionAccess) Repository() ocm.Repository { + r, _ := ocireg.NewRepository(a.GetContext(), "ghcr.io/component-model/ocm") + r, _ = r.Dup() + return r +} + +func (a *ComponentVersionAccess) GetDescriptor() *compdesc.ComponentDescriptor { + return &compdesc.ComponentDescriptor{ + Metadata: compdesc.Metadata{}, + ComponentSpec: compdesc.ComponentSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: "", + Version: "", + Labels: nil, + Provider: metav1.Provider{}, + CreationTime: nil, + }, + RepositoryContexts: nil, + Sources: nil, + References: nil, + Resources: nil, + }, + Signatures: nil, + NestedDigests: nil, + } +} + +func (a *ComponentVersionAccess) GetProvider() *compdesc.Provider { + return &a.GetDescriptor().Provider +} + +//////////////////////////////////////////////////////////////////////////////// + +type ResourceAccess ocm.ResourceMeta + +var _ ocm.ResourceAccess = (*ResourceAccess)(nil) + +func (r *ResourceAccess) Meta() *ocm.ResourceMeta { + return (*ocm.ResourceMeta)(r) +} + +func (r ResourceAccess) GetComponentVersion() (internal.ComponentVersionAccess, error) { + // TODO implement me + panic("implement me") +} + +func (r ResourceAccess) GetOCMContext() internal.Context { + // TODO implement me + panic("implement me") +} + +func (r ResourceAccess) ReferenceHint() string { + // TODO implement me + panic("implement me") +} + +func (r ResourceAccess) Access() (internal.AccessSpec, error) { + // TODO implement me + panic("implement me") +} + +func (r ResourceAccess) AccessMethod() (internal.AccessMethod, error) { + // TODO implement me + panic("implement me") +} + +func (r ResourceAccess) GlobalAccess() internal.AccessSpec { + // TODO implement me + panic("implement me") +} + +func (r ResourceAccess) BlobAccess() (blobaccess.BlobAccess, error) { + // TODO implement me + panic("implement me") +} diff --git a/api/ocm/tools/transfer/transferhandler/plugin/options.go b/api/ocm/tools/transfer/transferhandler/plugin/options.go new file mode 100644 index 0000000000..3960a303ce --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/options.go @@ -0,0 +1,174 @@ +package plugin + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" + "ocm.software/ocm/api/utils/runtime" +) + +func init() { + transferhandler.RegisterHandler(100, &TransferOptionsCreator{}) +} + +type Options struct { + standard.Options + plugin string + handler string + config interface{} +} + +var ( + _ transferhandler.TransferOption = (*Options)(nil) + _ transferhandler.ConfigOptionConsumer = (*Options)(nil) + + _ PluginNameOption = (*Options)(nil) + _ TransferHandlerOption = (*Options)(nil) + _ TransferHandlerConfigOption = (*Options)(nil) +) + +type TransferOptionsCreator = transferhandler.SpecializedOptionsCreator[*Options, Options] + +func (o *Options) NewTransferHandler() (transferhandler.TransferHandler, error) { + return New(o) +} + +func (o *Options) ApplyTransferOption(target transferhandler.TransferOptions) error { + if o.plugin != "" { + if opts, ok := target.(PluginNameOption); ok { + opts.SetPluginName(o.plugin) + } + } + if o.plugin != "" { + if opts, ok := target.(TransferHandlerOption); ok { + opts.SetTransferHandler(o.handler) + } + } + if o.config != nil { + if opts, ok := target.(TransferHandlerConfigOption); ok { + opts.SetTransferHandlerConfig(o.config) + } + } + return o.Options.ApplyTransferOption(target) +} + +func (o *Options) SetPluginName(name string) { + o.plugin = name +} + +func (o *Options) GetPluginName() string { + return o.plugin +} + +func (o *Options) SetTransferHandler(name string) { + o.handler = name +} + +func (o *Options) GetTransferHandler() string { + return o.handler +} + +func (o *Options) SetTransferHandlerConfig(cfg interface{}) { + o.config = cfg +} + +func (o *Options) GetTransferHandlerConfig() interface{} { + return o.config +} + +func (o *Options) SetConfig(bytes []byte) { + c, err := runtime.ToJSON(bytes) + if err != nil { + c, _ = json.Marshal(string(bytes)) //nolint:errchkjson // yes + } + o.config = c +} + +func (o *Options) GetConfig() []byte { + c, _ := runtime.ToJSON(o.config) + return c +} + +/////////////////////////////////////////////////////////////////////////////// + +type PluginNameOption interface { + SetPluginName(name string) + GetPluginName() string +} + +type pluginNameOption struct { + TransferOptionsCreator + name string +} + +func (o *pluginNameOption) ApplyTransferOption(to transferhandler.TransferOptions) error { + if eff, ok := to.(PluginNameOption); ok { + eff.SetPluginName(o.name) + return nil + } else { + return errors.ErrNotSupported(transferhandler.KIND_TRANSFEROPTION, "pluginName") + } +} + +func Plugin(name string) transferhandler.TransferOption { + return &pluginNameOption{ + name: name, + } +} + +/////////////////////////////////////////////////////////////////////////////// + +type TransferHandlerOption interface { + SetTransferHandler(name string) + GetTransferHandler() string +} + +type transferHandlerOption struct { + TransferOptionsCreator + name string +} + +func (o *transferHandlerOption) ApplyTransferOption(to transferhandler.TransferOptions) error { + if eff, ok := to.(TransferHandlerOption); ok { + eff.SetTransferHandler(o.name) + return nil + } else { + return errors.ErrNotSupported(transferhandler.KIND_TRANSFEROPTION, "transferHandler") + } +} + +func TransferHandler(name string) transferhandler.TransferOption { + return &transferHandlerOption{ + name: name, + } +} + +/////////////////////////////////////////////////////////////////////////////// + +type TransferHandlerConfigOption interface { + SetTransferHandlerConfig(cf interface{}) + GetTransferHandlerConfig() interface{} +} + +type transferHandlerConfigOption struct { + TransferOptionsCreator + config interface{} +} + +func (o *transferHandlerConfigOption) ApplyTransferOption(to transferhandler.TransferOptions) error { + if eff, ok := to.(TransferHandlerConfigOption); ok { + eff.SetTransferHandlerConfig(o.config) + return nil + } else { + return errors.ErrNotSupported(transferhandler.KIND_TRANSFEROPTION, "transferHandlerConfig") + } +} + +func TransferHandlerConfig(cfg interface{}) transferhandler.TransferOption { + return &transferHandlerConfigOption{ + config: cfg, + } +} diff --git a/api/ocm/tools/transfer/transferhandler/plugin/registration.go b/api/ocm/tools/transfer/transferhandler/plugin/registration.go new file mode 100644 index 0000000000..8cadcbeb63 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/registration.go @@ -0,0 +1,115 @@ +package plugin + +import ( + "fmt" + "slices" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" + "ocm.software/ocm/api/utils/registrations" +) + +type Config = interface{} + +func init() { + transferhandler.RegisterHandlerRegistrationHandler("plugin", &RegistrationHandler{}) +} + +type RegistrationHandler struct{} + +var _ transferhandler.ByNameCreationHandler = (*RegistrationHandler)(nil) + +func (r *RegistrationHandler) ByName(ctx ocm.Context, handler string, opts ...transferhandler.TransferOption) (bool, transferhandler.TransferHandler, error) { + path := cpi.NewNamePath(handler) + + if len(path) < 1 || len(path) > 2 { + return true, nil, fmt.Errorf("plugin handler name must be of the form [/]") + } + + name := "" + if len(path) > 1 { + name = path[1] + } + + h, err := CreateTransferHandler(ctx, path[0], name, opts...) + return true, h, err +} + +func CreateTransferHandler(ctx ocm.Context, pname, name string, opts ...transferhandler.TransferOption) (transferhandler.TransferHandler, error) { + options := &Options{} + err := transferhandler.ApplyOptions(options, append(slices.Clone(opts), Plugin(pname))...) + if err != nil { + return nil, err + } + + if options.plugin != "" && options.plugin != pname { + return nil, fmt.Errorf("plugin option not possible for path-based transferhandler creation") + } + if options.handler != "" && options.handler != name { + return nil, fmt.Errorf("plugin transfer handler option nor possible for path-based transferhandler creation") + } + + set := plugincacheattr.Get(ctx) + if set == nil { + return nil, errors.ErrUnknown(plugin.KIND_PLUGIN, pname) + } + + p := set.Get(pname) + if p == nil { + return nil, errors.ErrUnknown(plugin.KIND_PLUGIN, pname) + } + if name == "" { + names := p.GetTransferHandlerNames() + if len(names) == 0 { + return nil, fmt.Errorf("plugin %q does not provide transfer handlers", pname) + } + if len(names) != 1 { + return nil, fmt.Errorf("plugin %q provides more than one transfer handler", pname) + } + name = names[0] + } + d := p.GetTransferHandler(name) + if d == nil { + return nil, errors.ErrNotFound(plugin.KIND_TRANSFERHANDLER, name, pname) + } + TransferHandler(name).ApplyTransferOption(options) + + return &Handler{ + Handler: *standard.NewDefaultHandler(&options.Options), + opts: options, + plugin: p, + desc: d, + }, nil +} + +func (r *RegistrationHandler) GetHandlers(target *transferhandler.Target) registrations.HandlerInfos { + infos := registrations.NewNodeHandlerInfo("transfer handlers provided by plugins", + "sub namespace of the form <plugin name>/<handler>") + + set := plugincacheattr.Get(target.Context) + if set == nil { + return infos + } + + for _, name := range set.PluginNames() { + p := set.Get(name) + if !p.IsValid() { + continue + } + for _, d := range set.Get(name).GetDescriptor().Downloaders { + i := registrations.HandlerInfo{ + Name: name + "/" + d.GetName(), + ShortDesc: "", + Description: d.GetDescription(), + } + infos = append(infos, i) + } + } + return infos +} diff --git a/api/ocm/tools/transfer/transferhandler/plugin/suite_test.go b/api/ocm/tools/transfer/transferhandler/plugin/suite_test.go new file mode 100644 index 0000000000..b8526b774f --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/suite_test.go @@ -0,0 +1,13 @@ +package plugin_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "transfer handler plugin Test Suite") +} diff --git a/api/ocm/tools/transfer/transferhandler/plugin/testdata/config b/api/ocm/tools/transfer/transferhandler/plugin/testdata/config new file mode 100644 index 0000000000..338b2bcaab --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/testdata/config @@ -0,0 +1,5 @@ + +transferRepositories: + types: + ociArtifact: + - ghcr.io \ No newline at end of file diff --git a/api/ocm/tools/transfer/transferhandler/plugin/testdata/plugins/transferplugin b/api/ocm/tools/transfer/transferhandler/plugin/testdata/plugins/transferplugin new file mode 100755 index 0000000000..6b442407f6 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/testdata/plugins/transferplugin @@ -0,0 +1,2 @@ +#!/bin/bash +go run ../../../../../../cmds/transferplugin/main.go "$@" \ No newline at end of file diff --git a/api/ocm/tools/transfer/transferhandler/plugin/testdata/transferconfig b/api/ocm/tools/transfer/transferhandler/plugin/testdata/transferconfig new file mode 100644 index 0000000000..8be5ca90aa --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/testdata/transferconfig @@ -0,0 +1,5 @@ + +transferRepositories: + types: + ociArtifact: + - source.alias \ No newline at end of file diff --git a/api/ocm/tools/transfer/transferhandler/plugin/transfer_test.go b/api/ocm/tools/transfer/transferhandler/plugin/transfer_test.go new file mode 100644 index 0000000000..4ddebc15c1 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/plugin/transfer_test.go @@ -0,0 +1,156 @@ +package plugin_test + +import ( + "os" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/api/helper/builder" + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + . "ocm.software/ocm/api/ocm/plugin/testutils" + . "ocm.software/ocm/cmds/ocm/testhelper" + + "ocm.software/ocm/api/oci" + . "ocm.software/ocm/api/oci/testhelper" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/ocm/tools/transfer" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + common "ocm.software/ocm/api/utils/misc" +) + +const OCIPATH1 = "oci1" +const OCIPATH2 = "oci2" + +const OCIHOST1 = "source" +const OCIHOST2 = "target" + +const COMPONENT = "acme.org/component" +const VERSION = "1.0.0" +const PROVIDER = "acme.org" + +var _ = Describe("transport with plugin based transfer handler", func() { + var env *Builder + var plugins TempPluginDir + + BeforeEach(func() { + env = NewBuilder(TestData()) + plugins = Must(ConfigureTestPlugins(env, "testdata/plugins")) + + env.OCICommonTransport(OCIPATH1, accessio.FormatDirectory, func() { + OCIManifest1(env) + }) + + env.OCICommonTransport(OCIPATH2, accessio.FormatDirectory, func() { + OCIManifest1(env) + }) + + FakeOCIRepo(env, OCIPATH1, OCIHOST1) + FakeOCIRepo(env, OCIPATH2, OCIHOST2) + + env.OCMCommonTransport(OCIPATH1, accessio.FormatDirectory, func() { + env.Component(COMPONENT, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + env.Resource("image", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() { + env.Access( + ociartifact.New(oci.StandardOCIRef(OCIHOST1+".alias", OCINAMESPACE, OCIVERSION)), + ) + }) + }) + }) + }) + }) + + AfterEach(func() { + plugins.Cleanup() + env.Cleanup() + }) + + It("loads plugin", func() { + registry := plugincacheattr.Get(env) + // Expect(registration.RegisterExtensions(env)).To(Succeed()) + p := registry.Get(PLUGIN) + Expect(p).NotTo(BeNil()) + Expect(p.Error()).To(Equal("")) + }) + + DescribeTable("transfers per handler", func(cfgfile string) { + config := Must(os.ReadFile("testdata/" + cfgfile)) + p, buf := common.NewBufferedPrinter() + topts := append([]transferhandler.TransferOption{ + transfer.WithPrinter(p), + transferhandler.WithConfig(config), + }) + + h := Must(transferhandler.For(env).ByName(env, "plugin/transferplugin/demo", topts...)) + + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, OCIPATH1, 0, env)) + defer Close(src, "src") + + cv := Must(src.LookupComponentVersion(COMPONENT, VERSION)) + defer Close(cv, "cv") + + tgt := Must(ctf.Open(env, accessobj.ACC_WRITABLE, OCIPATH2, 0, env)) + ctgt := accessio.OnceCloser(tgt) + defer Close(ctgt, "tgt") + + MustBeSuccessful(transfer.TransferWithHandler(p, cv, tgt, h)) + + options := &standard.Options{} + transferhandler.ApplyOptions(options, topts...) + + out := ` + transferring version "acme.org/component:1.0.0"... + ...resource 0 image\[ociImage\]\(ocm/value:v2.0\)... + ...adding component version... +` + if cfgfile == "config" { + out = ` + transferring version "acme.org/component:1.0.0"... + ...adding component version... +` + } + Expect(string(buf.Bytes())).To(StringMatchTrimmedWithContext(utils.Crop(out, 2))) + MustBeSuccessful(ctgt.Close()) + + tgt = Must(ctf.Open(env, accessobj.ACC_READONLY, OCIPATH2, 0, env)) + ctgt = accessio.OnceCloser(tgt) + defer Close(ctgt, "tgt2") + + tcv := Must(tgt.LookupComponentVersion(COMPONENT, VERSION)) + ctcv := accessio.OnceCloser(tcv) + defer Close(ctcv, "tcv") + + r := Must(tcv.GetResourceByIndex(0)) + acc := Must(r.Access()) + + atype := localblob.Type + if cfgfile == "config" { + atype = ociartifact.Type + } + Expect(acc.GetKind()).To(Equal(atype)) + + info := acc.Info(env.OCMContext()) + + infotxt := "sha256:" + H_OCIARCHMANIFEST1 + if cfgfile == "config" { + infotxt = "ocm/value:v2.0" + } + Expect(info.Info).To(Equal(infotxt)) + + MustBeSuccessful(ctcv.Close()) + MustBeSuccessful(ctgt.Close()) + }, + Entry("with matching oci host", "transferconfig"), + Entry("without matching oci host", "config"), + ) +}) diff --git a/api/ocm/tools/transfer/transferhandler/registration.go b/api/ocm/tools/transfer/transferhandler/registration.go new file mode 100644 index 0000000000..feb18737f4 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/registration.go @@ -0,0 +1,133 @@ +package transferhandler + +import ( + "sort" + + "github.com/mandelsoft/goutils/general" + + "ocm.software/ocm/api/datacontext" + ocm "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/utils/listformat" + "ocm.software/ocm/api/utils/registrations" +) + +const ATTR_TRANSFER_HANDLERS = "ocm.software/ocm/api/ocm/tools/transfer/transferhandlers" + +func For(ctx ocm.ContextProvider) Registry { + if ctx == nil { + return DefaultRegistry + } + return ctx.OCMContext().GetAttributes().GetOrCreateAttribute(ATTR_TRANSFER_HANDLERS, create).(Registry) +} + +func create(datacontext.Context) interface{} { + return NewRegistry(DefaultRegistry) +} + +func SetFor(ctx datacontext.Context, registry Registry) { + ctx.GetAttributes().SetAttribute(ATTR_TRANSFER_HANDLERS, registry) +} + +//////////////////////////////////////////////////////////////////////////////// +// The creation handler is implemented by the RegistrationHandlerRegistry, +// but inÅ›tead of executing a handler registration, like for the downloaders, +// the created handler is just returned, + +type ByNameCreationHandler interface { + ByName(ctx ocm.Context, path string, olist ...TransferOption) (bool, TransferHandler, error) + GetHandlers(t *Target) registrations.HandlerInfos +} + +type HandlerInfos = registrations.HandlerInfos + +// byNameCreationHandler wraps the handler interface +// into the implementation type. +type byNameCreationHandler struct { + handler ByNameCreationHandler +} + +func (b byNameCreationHandler) GetHandlers(t *Target) HandlerInfos { + return b.handler.GetHandlers(t) +} + +func (b byNameCreationHandler) RegisterByName(path string, target *Target, _ registrations.HandlerConfig, opts ...TransferOption) (bool, error) { + ok, h, err := b.handler.ByName(target.Context, path, opts...) + target.Handler = h + return ok, err +} + +var _ registrations.HandlerRegistrationHandler[*Target, TransferOption] = (*byNameCreationHandler)(nil) + +//////////////////////////////////////////////////////////////////////////////// + +type Target struct { + Context ocm.Context + Handler TransferHandler +} + +func NewTarget(ctx ocm.ContextProvider) *Target { + return &Target{Context: ctx.OCMContext()} +} + +func RegisterHandlerRegistrationHandler(path string, handler ByNameCreationHandler) { + DefaultRegistry.RegisterRegistrationHandler(path, handler) +} + +func CreateByName(ctx ocm.ContextProvider, name string, opts ...TransferOption) (TransferHandler, error) { + return For(ctx).ByName(ctx, name, opts...) +} + +//////////////////////////////////////////////////////////////////////////////// + +var DefaultRegistry = NewRegistry() + +type Registry interface { + RegisterRegistrationHandler(path string, handler ByNameCreationHandler) + + ByName(ctx ocm.ContextProvider, name string, opts ...TransferOption) (TransferHandler, error) + + Copy() Registry + AsHandlerRegistrationRegistry() registrations.HandlerRegistrationRegistry[*Target, TransferOption] +} + +type _registry struct { + registry registrations.HandlerRegistrationRegistry[*Target, TransferOption] + base Registry +} + +func NewRegistry(base ...Registry) Registry { + b := general.Optional(base...) + return &_registry{registrations.NewHandlerRegistrationRegistry[*Target, TransferOption](asHandlerRegistrationRegistry(b)), b} +} + +func (r *_registry) RegisterRegistrationHandler(path string, handler ByNameCreationHandler) { + r.registry.RegisterRegistrationHandler(path, &byNameCreationHandler{handler}) +} + +func (r *_registry) ByName(ctx ocm.ContextProvider, name string, opts ...TransferOption) (TransferHandler, error) { + target := NewTarget(ctx.OCMContext()) + _, err := r.registry.RegisterByName(name, target, nil, opts...) + return target.Handler, err +} + +func (r *_registry) Copy() Registry { + return NewRegistry(r.base) +} + +func (r *_registry) AsHandlerRegistrationRegistry() registrations.HandlerRegistrationRegistry[*Target, TransferOption] { + return r.registry +} + +func asHandlerRegistrationRegistry(r Registry) registrations.HandlerRegistrationRegistry[*Target, TransferOption] { + if r == nil { + return nil + } + return r.AsHandlerRegistrationRegistry() +} + +func Usage(ctx ocm.Context) string { + list := For(ctx).AsHandlerRegistrationRegistry().GetHandlers(NewTarget(ctx)) + sort.Sort(list) + return `The following transfer handler names are supported: +` + listformat.FormatListElements("", list) +} diff --git a/api/ocm/tools/transfer/transferhandler/registration_test.go b/api/ocm/tools/transfer/transferhandler/registration_test.go new file mode 100644 index 0000000000..88896a9914 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/registration_test.go @@ -0,0 +1,39 @@ +package transferhandler_test + +import ( + "reflect" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + + _ "ocm.software/ocm/api/ocm/tools/transfer" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/generics" + + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/spiff" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" +) + +var _ = Describe("Registration Test Environment", func() { + ctx := ocm.New(datacontext.MODE_EXTENDED) + + It("standard", func() { + h := Must(transferhandler.For(ctx).ByName(ctx, "ocm/standard")) + Expect(reflect.TypeOf(h)).To(Equal(generics.TypeOf[*standard.Handler]())) + }) + + It("spiff", func() { + h := Must(transferhandler.For(ctx).ByName(ctx, "ocm/spiff")) + Expect(reflect.TypeOf(h)).To(Equal(generics.TypeOf[*spiff.Handler]())) + }) + + It("plugin", func() { + ExpectError(transferhandler.For(ctx).ByName(ctx, "plugin/p/h")).To(MatchError(errors.ErrUnknown(plugin.KIND_PLUGIN, "p"))) + }) +}) diff --git a/api/ocm/tools/transfer/transferhandler/setup.go b/api/ocm/tools/transfer/transferhandler/setup.go new file mode 100644 index 0000000000..15a3c1e959 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/setup.go @@ -0,0 +1,27 @@ +package transferhandler + +import ( + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/ocm/cpi" +) + +func init() { + datacontext.RegisterSetupHandler(datacontext.SetupHandlerFunction(setupContext)) +} + +func setupContext(mode datacontext.BuilderMode, ctx datacontext.Context) { + if octx, ok := ctx.(cpi.Context); ok { + switch mode { + case datacontext.MODE_SHARED: + fallthrough + case datacontext.MODE_DEFAULTED: + // do nothing, fallback to the default attribute lookup + case datacontext.MODE_EXTENDED: + SetFor(octx, NewRegistry(DefaultRegistry)) + case datacontext.MODE_CONFIGURED: + SetFor(octx, DefaultRegistry.Copy()) + case datacontext.MODE_INITIAL: + SetFor(octx, NewRegistry()) + } + } +} diff --git a/api/ocm/tools/transfer/transferhandler/spiff/handler.go b/api/ocm/tools/transfer/transferhandler/spiff/handler.go index 4044ffdf95..040572ff62 100644 --- a/api/ocm/tools/transfer/transferhandler/spiff/handler.go +++ b/api/ocm/tools/transfer/transferhandler/spiff/handler.go @@ -41,6 +41,10 @@ func New(opts ...transferhandler.TransferOption) (transferhandler.TransferHandle }, nil } +func (h *Handler) GetScript() []byte { + return h.opts.GetScript() +} + // TODO: handle update and overwrite per script func (h *Handler) UpdateVersion(src ocm.ComponentVersionAccess, tgt ocm.ComponentVersionAccess) (bool, error) { @@ -50,7 +54,7 @@ func (h *Handler) UpdateVersion(src ocm.ComponentVersionAccess, tgt ocm.Componen if h.opts.GetScript() == nil { return false, nil } - binding := h.getBinding(src, nil, nil, nil, nil) + binding := h.getBinding(src, nil, nil, nil, tgt.Repository()) return h.EvalBool("update", binding, "process") } @@ -61,7 +65,7 @@ func (h *Handler) EnforceTransport(src ocm.ComponentVersionAccess, tgt ocm.Compo if h.opts.GetScript() == nil { return false, nil } - binding := h.getBinding(src, nil, nil, nil, nil) + binding := h.getBinding(src, nil, nil, nil, tgt.Repository()) return h.EvalBool("enforceTransport", binding, "process") } @@ -72,7 +76,7 @@ func (h *Handler) OverwriteVersion(src ocm.ComponentVersionAccess, tgt ocm.Compo if h.opts.GetScript() == nil { return false, nil } - binding := h.getBinding(src, nil, nil, nil, nil) + binding := h.getBinding(src, nil, nil, nil, tgt.Repository()) return h.EvalBool("overwrite", binding, "process") } diff --git a/api/ocm/tools/transfer/transferhandler/spiff/handler_test.go b/api/ocm/tools/transfer/transferhandler/spiff/handler_test.go index 72e2e5aa94..c371b2ab54 100644 --- a/api/ocm/tools/transfer/transferhandler/spiff/handler_test.go +++ b/api/ocm/tools/transfer/transferhandler/spiff/handler_test.go @@ -3,13 +3,17 @@ package spiff_test import ( "encoding/json" "fmt" + "reflect" + "github.com/mandelsoft/goutils/generics" + . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "ocm.software/ocm/api/datacontext" . "ocm.software/ocm/api/helper/builder" . "ocm.software/ocm/api/oci/testhelper" - - "github.com/mandelsoft/goutils/testutils" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" "ocm.software/ocm/api/oci" "ocm.software/ocm/api/oci/artdesc" @@ -18,7 +22,7 @@ import ( "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" "ocm.software/ocm/api/ocm/extensions/repositories/ctf" - ocmutils "ocm.software/ocm/api/ocm/ocmutils" + "ocm.software/ocm/api/ocm/ocmutils" "ocm.software/ocm/api/ocm/tools/transfer" "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/spiff" "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" @@ -197,7 +201,7 @@ process: (( (*(rules[mode] || rules.default)).process )) Expect(err).To(Succeed()) fmt.Printf("%s\n", string(data)) hash := HashManifest2(artifactset.DefaultArtifactSetDescriptorFileName) - Expect(string(data)).To(testutils.StringEqualWithContext("{\"localReference\":\"" + hash + "\",\"mediaType\":\"application/vnd.oci.image.manifest.v1+tar+gzip\",\"referenceName\":\"" + OCINAMESPACE2 + ":" + OCIVERSION + "\",\"type\":\"localBlob\"}")) + Expect(string(data)).To(StringEqualWithContext("{\"localReference\":\"" + hash + "\",\"mediaType\":\"application/vnd.oci.image.manifest.v1+tar+gzip\",\"referenceName\":\"" + OCINAMESPACE2 + ":" + OCIVERSION + "\",\"type\":\"localBlob\"}")) data, err = json.Marshal(comp.GetDescriptor().Resources[1].Access) Expect(err).To(Succeed()) @@ -295,4 +299,15 @@ process: (( (*(rules[mode] || rules.default)).process )) Expect(err).To(Succeed()) }) }) + + Context("registrations", func() { + ctx := ocm.New(datacontext.MODE_EXTENDED) + + It("finds plugin handler by name and config", func() { + h := Must(transferhandler.For(ctx).ByName(ctx, "ocm/spiff", transferhandler.WithConfig([]byte(script1)))) + Expect(reflect.TypeOf(h)).To(Equal(generics.TypeOf[*spiff.Handler]())) + Expect(string(h.(*spiff.Handler).GetScript())).To(Equal(script1)) + }) + }) + }) diff --git a/api/ocm/tools/transfer/transferhandler/spiff/options.go b/api/ocm/tools/transfer/transferhandler/spiff/options.go index 68ba1c2aa4..d05d65a9b3 100644 --- a/api/ocm/tools/transfer/transferhandler/spiff/options.go +++ b/api/ocm/tools/transfer/transferhandler/spiff/options.go @@ -29,10 +29,6 @@ var ( type TransferOptionsCreator = transferhandler.SpecializedOptionsCreator[*Options, Options] -func (o *Options) NewOptions() transferhandler.TransferHandlerOptions { - return &Options{} -} - func (o *Options) NewTransferHandler() (transferhandler.TransferHandler, error) { return New(o) } @@ -121,6 +117,14 @@ func ScriptByFile(path string, fss ...vfs.FileSystem) transferhandler.TransferOp } } +func (o *Options) SetConfig(bytes []byte) { + o.script = bytes +} + +func (o *Options) GetConfig() []byte { + return o.script +} + /////////////////////////////////////////////////////////////////////////////// type ScriptFilesystemOption interface { diff --git a/api/ocm/tools/transfer/transferhandler/spiff/registration.go b/api/ocm/tools/transfer/transferhandler/spiff/registration.go new file mode 100644 index 0000000000..332cbcbef5 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/spiff/registration.go @@ -0,0 +1,33 @@ +package spiff + +import ( + "fmt" + + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/utils/registrations" +) + +func init() { + transferhandler.RegisterHandlerRegistrationHandler("ocm/spiff", &RegistrationHandler{}) +} + +type RegistrationHandler struct{} + +var _ transferhandler.ByNameCreationHandler = (*RegistrationHandler)(nil) + +func (r *RegistrationHandler) ByName(ctx ocm.Context, path string, olist ...transferhandler.TransferOption) (bool, transferhandler.TransferHandler, error) { + if path != "" { + return true, nil, fmt.Errorf("invalid standard handler %q", path) + } + + h, err := New(olist...) + return true, h, err +} + +func (r *RegistrationHandler) GetHandlers(target *transferhandler.Target) registrations.HandlerInfos { + return registrations.NewLeafHandlerInfo("spiff transfer handler", ` +The spiff transfer handler works on the standard transfer options +extended by dynamic programming based on spiff++.`, + ) +} diff --git a/api/ocm/tools/transfer/transferhandler/standard/options.go b/api/ocm/tools/transfer/transferhandler/standard/options.go index bfc0524753..25abc503b9 100644 --- a/api/ocm/tools/transfer/transferhandler/standard/options.go +++ b/api/ocm/tools/transfer/transferhandler/standard/options.go @@ -52,10 +52,6 @@ var ( type TransferOptionsCreator = transferhandler.SpecializedOptionsCreator[*Options, Options] -func (o *Options) NewOptions() transferhandler.TransferHandlerOptions { - return &Options{} -} - func (o *Options) NewTransferHandler() (transferhandler.TransferHandler, error) { return New(o) } @@ -141,6 +137,10 @@ func (o *Options) IsTransportEnforced() bool { return optionutils.AsBool(o.enforceTransport) } +func (o *Options) GetEnforceTransport() *bool { + return o.enforceTransport +} + func (o *Options) SetOverwrite(overwrite bool) { o.overwrite = &overwrite } @@ -149,6 +149,10 @@ func (o *Options) IsOverwrite() bool { return optionutils.AsBool(o.overwrite) } +func (o *Options) GetOverwrite() *bool { + return o.overwrite +} + func (o *Options) SetSkipUpdate(skipupdate bool) { o.skipUpdate = &skipupdate } @@ -157,6 +161,10 @@ func (o *Options) IsSkipUpdate() bool { return optionutils.AsBool(o.skipUpdate) } +func (o *Options) GetSkipUpdate() *bool { + return o.skipUpdate +} + func (o *Options) SetRecursive(recursive bool) { o.recursive = &recursive } @@ -165,6 +173,10 @@ func (o *Options) IsRecursive() bool { return optionutils.AsBool(o.recursive) } +func (o *Options) GetRecursive() *bool { + return o.recursive +} + func (o *Options) SetResourcesByValue(resourcesByValue bool) { o.resourcesByValue = &resourcesByValue } @@ -173,6 +185,10 @@ func (o *Options) IsResourcesByValue() bool { return optionutils.AsBool(o.resourcesByValue) } +func (o *Options) GetResourcesByValue() *bool { + return o.resourcesByValue +} + func (o *Options) SetLocalResourcesByValue(resourcesByValue bool) { o.localByValue = &resourcesByValue } @@ -181,6 +197,10 @@ func (o *Options) IsLocalResourcesByValue() bool { return optionutils.AsBool(o.localByValue) } +func (o *Options) GetLocalResourcesByValue() *bool { + return o.localByValue +} + func (o *Options) SetSourcesByValue(sourcesByValue bool) { o.sourcesByValue = &sourcesByValue } @@ -189,6 +209,10 @@ func (o *Options) IsSourcesByValue() bool { return optionutils.AsBool(o.sourcesByValue) } +func (o *Options) GetSourcesByValue() *bool { + return o.sourcesByValue +} + func (o *Options) SetKeepGlobalAccess(keepGlobalAccess bool) { o.keepGlobalAccess = &keepGlobalAccess } @@ -197,6 +221,10 @@ func (o *Options) IsKeepGlobalAccess() bool { return optionutils.AsBool(o.keepGlobalAccess) } +func (o *Options) GetKeepGlobalAccess() *bool { + return o.keepGlobalAccess +} + func (o *Options) SetRetries(retries int) { o.retries = &retries } @@ -224,6 +252,10 @@ func (o *Options) IsStopOnExistingVersion() bool { return optionutils.AsBool(o.stopOnExisting) } +func (o *Options) GetStopOnExistingVersion() *bool { + return o.stopOnExisting +} + func (o *Options) SetOmittedAccessTypes(list ...string) { o.omitAccessTypes = set.New[string]() for _, t := range list { diff --git a/api/ocm/tools/transfer/transferhandler/standard/registration.go b/api/ocm/tools/transfer/transferhandler/standard/registration.go new file mode 100644 index 0000000000..1081e38ac0 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/standard/registration.go @@ -0,0 +1,32 @@ +package standard + +import ( + "fmt" + + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/utils/registrations" +) + +func init() { + transferhandler.RegisterHandlerRegistrationHandler("ocm/standard", &RegistrationHandler{}) +} + +type RegistrationHandler struct{} + +var _ transferhandler.ByNameCreationHandler = (*RegistrationHandler)(nil) + +func (r *RegistrationHandler) ByName(ctx ocm.Context, path string, olist ...transferhandler.TransferOption) (bool, transferhandler.TransferHandler, error) { + if path != "" { + return true, nil, fmt.Errorf("invalid standard handler %q", path) + } + + h, err := New(olist...) + return true, h, err +} + +func (r *RegistrationHandler) GetHandlers(target *transferhandler.Target) registrations.HandlerInfos { + return registrations.NewLeafHandlerInfo("standard transfer handler", ` +The standard transfer handler works on the standard transfer options.`, + ) +} diff --git a/api/ocm/tools/transfer/transferhandler/standard/transfer_test.go b/api/ocm/tools/transfer/transferhandler/standard/transfer_test.go new file mode 100644 index 0000000000..ed86c529af --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/standard/transfer_test.go @@ -0,0 +1,305 @@ +package standard_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/api/helper/builder" + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" + ocictf "ocm.software/ocm/api/oci/extensions/repositories/ctf" + . "ocm.software/ocm/api/oci/testhelper" + "ocm.software/ocm/api/ocm" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "ocm.software/ocm/api/ocm/extensions/accessmethods/relativeociref" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/ocm/extensions/attrs/keepblobattr" + "ocm.software/ocm/api/ocm/extensions/blobhandler" + storagecontext "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci" + "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci/ocirepo" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/ocm/tools/transfer" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + common "ocm.software/ocm/api/utils/misc" +) + +const OCIHOST2 = "target" + +var _ = Describe("value transport with relative ocireg", func() { + var env *Builder + + BeforeEach(func() { + env = NewBuilder() + + env.RSAKeyPair(SIGNATURE) + + env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() { + OCIManifest1(env) + }) + + FakeOCIRepo(env, OCIPATH, OCIHOST) + FakeOCIRepo(env, ARCH, OCIHOST2) + + env.OCMCommonTransport(OCIPATH, accessio.FormatDirectory, func() { + env.Component(COMPONENT, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + env.Resource("artifact", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() { + env.Access( + relativeociref.New(oci.RelativeOCIRef(OCINAMESPACE, OCIVERSION)), + ) + }) + }) + }) + }) + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("it should use additional resolver to resolve component ref", func() { + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, OCIPATH, 0, env)) + defer Close(src, "src") + + cv := Must(src.LookupComponentVersion(COMPONENT, VERSION)) + defer Close(cv, "cv") + + r := Must(cv.GetResourceByIndex(0)) + CheckBlob(r, D_OCIMANIFEST1, 628) + }) + + DescribeTable("transfers per value", func(keep bool, mod func(env *Builder), dig string, size int, opts ...transferhandler.TransferOption) { + env.OCMContext().BlobHandlers().Register(ocirepo.NewArtifactHandler(FakeOCIRegBaseFunction), + blobhandler.ForRepo(oci.CONTEXT_TYPE, ocictf.Type), blobhandler.ForMimeType(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest))) + keepblobattr.Set(env.OCMContext(), keep) + + p, buf := common.NewBufferedPrinter() + topts := append([]transferhandler.TransferOption{ + standard.ResourcesByValue(), transfer.WithPrinter(p), + }, opts...) + + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, OCIPATH, 0, env)) + defer Close(src, "src") + + cv := Must(src.LookupComponentVersion(COMPONENT, VERSION)) + defer Close(cv, "cv") + + mod(env) + + tgt := Must(ctf.Open(env, accessobj.ACC_WRITABLE, ARCH, 0, env)) + ctgt := accessio.OnceCloser(tgt) + defer Close(ctgt, "tgt") + + MustBeSuccessful(transfer.Transfer(cv, tgt, topts...)) + + options := &standard.Options{} + transferhandler.ApplyOptions(options, topts...) + + out := ` + transferring version "github.com/mandelsoft/test:v1"... + ...resource 0 artifact\[ociImage\]\(ocm/value:v2.0\)... + ...adding component version... +` + if options.IsOverwrite() { + out = ` + transferring version "github.com/mandelsoft/test:v1"... + warning: version "github.com/mandelsoft/test:v1" already present, but differs because some artifact.*changed \(transport enforced by overwrite option\) + ...resource 0 artifact\[ociImage\]\(ocm/value:v2.0\).* + ...adding component version... +` + } + Expect(string(buf.Bytes())).To(StringMatchTrimmedWithContext(utils.Crop(out, 2))) + MustBeSuccessful(ctgt.Close()) + + tgt = Must(ctf.Open(env, accessobj.ACC_READONLY, ARCH, 0, env)) + ctgt = accessio.OnceCloser(tgt) + defer Close(ctgt, "tgt2") + + tcv := Must(tgt.LookupComponentVersion(COMPONENT, VERSION)) + ctcv := accessio.OnceCloser(tcv) + defer Close(ctcv, "tcv") + + r := Must(tcv.GetResourceByIndex(0)) + acc := Must(r.Access()) + + atype := ociartifact.Type + if keep { + atype = localblob.Type + } + Expect(acc.GetKind()).To(Equal(atype)) + + info := acc.Info(env.OCMContext()) + if keep { + Expect(info.Info).To(Equal("sha256:" + H_OCIARCHMANIFEST1)) + + } else { + Expect(info.Host).To(Equal(OCIHOST2 + ".alias")) + Expect(info.Info).To(Equal("ocm/value:v2.0@sha256:" + D_OCIMANIFEST1)) + } + + CheckBlob(r, dig, size) + + MustBeSuccessful(ctcv.Close()) + MustBeSuccessful(ctgt.Close()) + + CheckAritifact(env, dig, size) + + // re-transport + buf.Reset() + tgt = Must(ctf.Open(env, accessobj.ACC_WRITABLE, ARCH, 0, env)) + ctgt = accessio.OnceCloser(tgt) + defer Close(ctgt, "tgt3") + + MustBeSuccessful(transfer.Transfer(cv, tgt, topts...)) + + tcv = Must(tgt.LookupComponentVersion(COMPONENT, VERSION)) + ctcv = accessio.OnceCloser(tcv) + defer Close(ctcv, "ctcv") + + r = Must(tcv.GetResourceByIndex(0)) + acc = Must(r.Access()) + Expect(acc.GetKind()).To(Equal(atype)) + + info = acc.Info(env.OCMContext()) + if keep { + Expect(info.Info).To(Equal("sha256:" + H_OCIARCHMANIFEST1)) + + } else { + Expect(info.Info).To(Equal("ocm/value:v2.0@sha256:" + D_OCIMANIFEST1)) + } + + MustBeSuccessful(ctcv.Close()) + MustBeSuccessful(ctgt.Close()) + + CheckAritifact(env, dig, size) + + }, + Entry("empty target", false, EmptyTarget, D_OCIMANIFEST1, 628), + Entry("identical target", false, IdenticalTarget, D_OCIMANIFEST1, 628), + Entry("different target", false, DifferentTarget, D_OCIMANIFEST1, 628), + Entry("different CV", false, DifferentCV, D_OCIMANIFEST1, 628, standard.Overwrite()), + Entry("different namespace", false, DifferentNamespace, D_OCIMANIFEST1, 628, standard.Overwrite()), + Entry("different name", false, DifferentName, D_OCIMANIFEST1, 628, standard.Overwrite()), + Entry("keep, empty target", true, EmptyTarget, D_OCIMANIFEST1, 628), + + Entry("keep, identical target", true, IdenticalTarget, D_OCIMANIFEST1, 628), + Entry("keep, different target", true, DifferentTarget, D_OCIMANIFEST1, 628), + Entry("keep, different CV", true, DifferentCV, D_OCIMANIFEST1, 628, standard.Overwrite()), + Entry("keep, different namespace", true, DifferentNamespace, D_OCIMANIFEST1, 628, standard.Overwrite()), + Entry("keep, different name", true, DifferentName, D_OCIMANIFEST1, 628, standard.Overwrite()), + ) +}) + +func EmptyTarget(env *Builder) { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory) +} + +func IdenticalTarget(env *Builder) { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + OCIManifest1For(env, OCINAMESPACE, OCIVERSION) + }) +} + +func DifferentTarget(env *Builder) { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + OCIManifest2For(env, OCINAMESPACE, OCIVERSION) + }) +} + +func DifferentCV(env *Builder) { + env.OCICommonTransport(ARCH, accessio.FormatDirectory, func() { + OCIManifest2For(env, OCINAMESPACE, OCIVERSION) + }) + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.Component(COMPONENT, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + env.Resource("artifact", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() { + env.Access( + ociartifact.New(oci.StandardOCIRef(OCIHOST2+".alias", OCINAMESPACE, OCIVERSION)), + ) + }) + }) + }) + }) +} + +func DifferentNamespace(env *Builder) { + env.OCICommonTransport(ARCH, accessio.FormatDirectory, func() { + OCIManifest2For(env, OCINAMESPACE2, OCIVERSION) + }) + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.Component(COMPONENT, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + env.Resource("artifact", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() { + env.Access( + relativeociref.New(oci.RelativeOCIRef(OCINAMESPACE2, OCIVERSION)), + ) + }) + }) + }) + }) +} + +func DifferentName(env *Builder) { + env.OCICommonTransport(ARCH, accessio.FormatDirectory, func() { + OCIManifest1For(env, OCINAMESPACE, OCIVERSION) + }) + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.Component(COMPONENT, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + env.Resource("other", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() { + env.Access( + ociartifact.New(oci.StandardOCIRef(OCIHOST2+".alias", OCINAMESPACE, OCIVERSION)), + ) + }) + }) + }) + }) +} + +func FakeOCIRegBaseFunction(ctx *storagecontext.StorageContext) string { + return OCIHOST2 + ".alias" +} + +func CheckBlob(r ocm.ResourceAccess, dig string, size int) { + blob := Must(r.BlobAccess()) + defer Close(blob, "blob") + + ExpectWithOffset(1, int(blob.Size())).To(Equal(size)) + set := Must(artifactset.OpenFromBlob(accessobj.ACC_READONLY, blob)) + defer Close(set, "set") + + // data := Must(json.Marshal(set.GetIndex())) + // fmt.Printf("%s\n", string(data)) + + digest := set.GetMain() + ExpectWithOffset(1, digest.Hex()).To(Equal(dig)) + + acc := Must(set.GetArtifact(digest.String())) + defer Close(acc, "acc") + + ExpectWithOffset(1, acc.IsManifest()).To(BeTrue()) + ExpectWithOffset(1, acc.Digest().Hex()).To(Equal(dig)) +} + +func CheckAritifact(env *Builder, dig string, size int) { + repo := Must(ocictf.Open(env, accessobj.ACC_READONLY, ARCH, 0, env)) + defer Close(repo, "oci repo") + + art := Must(repo.LookupArtifact(OCINAMESPACE, OCIVERSION)) + defer Close(art, "art") + + ExpectWithOffset(1, art.IsManifest()).To(BeTrue()) + ExpectWithOffset(1, art.Digest().Hex()).To(Equal(dig)) +} diff --git a/api/ocm/tools/transfer/transferhandler/suite_test.go b/api/ocm/tools/transfer/transferhandler/suite_test.go new file mode 100644 index 0000000000..0998312604 --- /dev/null +++ b/api/ocm/tools/transfer/transferhandler/suite_test.go @@ -0,0 +1,13 @@ +package transferhandler_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Transfer Handler Test Suite") +} diff --git a/api/tech/access.go b/api/tech/access.go new file mode 100644 index 0000000000..0da2e8577c --- /dev/null +++ b/api/tech/access.go @@ -0,0 +1,16 @@ +package tech + +// UniformAccessSpecInfo describes a rough uniform specification for +// an access location or an accessed object. It does not necessarily +// provide the exact access information required to technically +// access the object, but just some general information usable +// independently of the particular technical access specification +// to figure out some general information in a formal way about the access. +type UniformAccessSpecInfo struct { + Kind string `json:"kind"` + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Path string `json:"path,omitempty"` + + Info string `json:"info,omitempty"` +} diff --git a/api/utils/accessio/limitwriter.go b/api/utils/accessio/limitwriter.go index 555f351404..85e26c2232 100644 --- a/api/utils/accessio/limitwriter.go +++ b/api/utils/accessio/limitwriter.go @@ -46,7 +46,7 @@ type LimitedBuffer struct { } func (b *LimitedBuffer) Exceeded() bool { - return b.LimitedWriter.N > b.max + return b.LimitedWriter.N < 0 } func (b *LimitedBuffer) Bytes() []byte { diff --git a/api/utils/runtime/json.go b/api/utils/runtime/json.go new file mode 100644 index 0000000000..fe5f175aa5 --- /dev/null +++ b/api/utils/runtime/json.go @@ -0,0 +1,30 @@ +package runtime + +import ( + "encoding/json" +) + +func ToJSON(in interface{}) (json.RawMessage, error) { + if in == nil { + return nil, nil + } + + var raw interface{} + switch c := in.(type) { + case json.RawMessage: + return c, nil + case []byte: + err := DefaultYAMLEncoding.Unmarshal(c, &raw) + if err != nil { + return nil, err + } + case string: + err := DefaultYAMLEncoding.Unmarshal([]byte(c), &raw) + if err != nil { + return nil, err + } + default: + raw = c + } + return json.Marshal(raw) +} diff --git a/api/utils/utils.go b/api/utils/utils.go index b51a48bce2..bdffb1d2f8 100644 --- a/api/utils/utils.go +++ b/api/utils/utils.go @@ -355,3 +355,13 @@ func AsBool(b *bool, def ...bool) bool { } return b != nil && *b } + +func Crop(s string, n int) string { + lines := strings.Split(s, "\n") + for i, l := range lines { + if len(l) >= 2 { + lines[i] = l[n:] + } + } + return strings.Join(lines, "\n") +} diff --git a/cmds/demoplugin/accessmethods/demo.go b/cmds/demoplugin/accessmethods/demo.go index 3cce5f5fae..cb41f5acf5 100644 --- a/cmds/demoplugin/accessmethods/demo.go +++ b/cmds/demoplugin/accessmethods/demo.go @@ -13,6 +13,7 @@ import ( "ocm.software/ocm/api/credentials/cpi" "ocm.software/ocm/api/ocm/extensions/accessmethods/options" "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/tech" "ocm.software/ocm/api/tech/oci/identity" "ocm.software/ocm/api/utils/cobrautils/flagsets" "ocm.software/ocm/api/utils/runtime" @@ -87,6 +88,11 @@ func (a *AccessMethod) ValidateSpecification(p ppi.Plugin, spec ppi.AccessSpec) } info.Short = "temp file " + my.Path info.Hint = "temp file " + my.Path + // optional information for extended access spec interface + info.Info = &tech.UniformAccessSpecInfo{ + Kind: my.GetKind(), + Info: my.Path, + } return &info, nil } diff --git a/cmds/ocm/commands/ocmcmds/common/options/scriptoption/option.go b/cmds/ocm/commands/ocmcmds/common/options/scriptoption/option.go index ff8acf3c7d..c9b2111314 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/scriptoption/option.go +++ b/cmds/ocm/commands/ocmcmds/common/options/scriptoption/option.go @@ -109,5 +109,8 @@ func (o *Option) ApplyTransferOption(opts transferhandler.TransferOptions) error } else if o.ScriptFile != "" { err = spiff.ScriptByFile(o.ScriptFile, o.FileSystem).ApplyTransferOption(opts) } + if err == nil && o.FileSystem != nil && (o.ScriptData != nil || o.ScriptFile != "") { + err = spiff.ScriptFilesystem(o.FileSystem).ApplyTransferOption(opts) + } return err } diff --git a/cmds/ocm/commands/ocmcmds/common/options/transferhandleroption/option.go b/cmds/ocm/commands/ocmcmds/common/options/transferhandleroption/option.go new file mode 100644 index 0000000000..1f635002a6 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/transferhandleroption/option.go @@ -0,0 +1,74 @@ +package transferhandleroption + +import ( + "strings" + + "github.com/spf13/pflag" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/cmds/ocm/common/options" +) + +func From(o options.OptionSetProvider) *Option { + var opt *Option + o.AsOptionSet().Get(&opt) + return opt +} + +func New() *Option { + return &Option{} +} + +type Option struct { + setting string + Path string + Config []byte +} + +var ( + _ options.OptionWithCLIContextCompleter = (*Option)(nil) + _ transferhandler.TransferOption = (*Option)(nil) +) + +func (o *Option) AddFlags(fs *pflag.FlagSet) { + fs.StringVarP(&o.setting, "transfer-handler", "T", "", "transfer handler ([=)") +} + +func (o *Option) Configure(ctx clictx.Context) error { + if o.setting == "" { + return nil + } + idx := strings.Index(o.setting, "=") + if idx >= 0 { + o.Path = o.setting[:idx] + data, err := utils.ResolveData(o.setting[idx+1:], vfsattr.Get(ctx)) + if err != nil { + return err + } + o.Config = data + } else { + o.Path = o.setting + } + return nil +} + +func (o *Option) Usage() string { + s := ` +It is possible to use dedicated transfer handlers, either built-in ones or +plugin based handlers. The option --transferHandler can be used to specify +this handler using the hierarchical handler notation scheme. +` + transferhandler.Usage(ocm.DefaultContext()) + return s +} + +func (o *Option) ApplyTransferOption(opts transferhandler.TransferOptions) error { + var err error + if len(o.Config) != 0 { + err = transferhandler.WithConfig(o.Config).ApplyTransferOption(opts) + } + return err +} diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go b/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go index d01c477bf9..2cfce117e0 100644 --- a/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go +++ b/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go @@ -14,7 +14,6 @@ import ( "ocm.software/ocm/api/ocm" "ocm.software/ocm/api/ocm/tools/transfer" "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" - "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/spiff" common "ocm.software/ocm/api/utils/misc" "ocm.software/ocm/api/utils/out" "ocm.software/ocm/cmds/ocm/commands/common/options/closureoption" @@ -30,11 +29,11 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/skipupdateoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/srcbyvalueoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/stoponexistingoption" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/transferhandleroption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/uploaderoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/versionconstraintsoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" "ocm.software/ocm/cmds/ocm/commands/verbs" - "ocm.software/ocm/cmds/ocm/common/options" "ocm.software/ocm/cmds/ocm/common/output" "ocm.software/ocm/cmds/ocm/common/utils" ) @@ -69,6 +68,7 @@ func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { stoponexistingoption.New(), uploaderoption.New(ctx.OCMContext()), scriptoption.New(), + transferhandleroption.New(), )}, utils.Names(Names, names...)...) } @@ -103,7 +103,8 @@ func (o *Command) Complete(args []string) error { return fmt.Errorf("a repository or at least one argument that defines the reference is required") } o.TargetName = args[len(args)-1] - return nil + + return ValidateHandler(o) } func (o *Command) Run() error { @@ -130,13 +131,7 @@ func (o *Command) Run() error { return err } - transferopts := &spiff.Options{} - transferhandler.From(o.ConfigContext(), transferopts) - transferhandler.ApplyOptions(transferopts, append(options.FindOptions[transferhandler.TransferOption](o), - spiff.Script(scriptoption.From(o).ScriptData), - spiff.ScriptFilesystem(o.FileSystem()), - )...) - thdlr, err := spiff.New(transferopts) + thdlr, err := DetermineTransferHandler(o.Context, o) if err != nil { return err } diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/cmd_test.go b/cmds/ocm/commands/ocmcmds/components/transfer/cmd_test.go index 51948a7ad2..b0ccbc0a0f 100644 --- a/cmds/ocm/commands/ocmcmds/components/transfer/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/components/transfer/cmd_test.go @@ -3,11 +3,14 @@ package transfer_test import ( "bytes" "encoding/json" + "fmt" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "ocm.software/ocm/api/oci/testhelper" + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + . "ocm.software/ocm/api/ocm/plugin/testutils" . "ocm.software/ocm/cmds/ocm/testhelper" "github.com/spf13/cobra" @@ -22,7 +25,7 @@ import ( "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" ctfocm "ocm.software/ocm/api/ocm/extensions/repositories/ctf" - ocmutils "ocm.software/ocm/api/ocm/ocmutils" + "ocm.software/ocm/api/ocm/ocmutils" handlercfg "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/config" "ocm.software/ocm/api/utils" "ocm.software/ocm/api/utils/accessio" @@ -42,6 +45,11 @@ const ( OCIHOST = "alias" ) +const ( + PLUGIN = "transferplugin" + HANDLER = "demo" +) + func CheckComponentInArchive(env *TestEnv, ldesc *artdesc.Descriptor, out string) { tgt, err := ctfocm.Open(env.OCMContext(), accessobj.ACC_READONLY, out, 0, accessio.PathFileSystem(env.FileSystem())) Expect(err).To(Succeed()) @@ -94,7 +102,7 @@ var _ = Describe("Test Environment", func() { _ = ldesc BeforeEach(func() { - env = NewTestEnv() + env = NewTestEnv(TestData()) FakeOCIRepo(env.Builder, OCIPATH, OCIHOST) @@ -114,7 +122,6 @@ var _ = Describe("Test Environment", func() { env.Access( ociartifact.New(oci.StandardOCIRef(OCIHOST+".alias", OCINAMESPACE, OCIVERSION)), ) - env.Label("transportByValue", true) }) env.Resource("ref", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() { env.Access( @@ -158,6 +165,23 @@ transferring version "github.com/mandelsoft/test:v1"... CheckComponentInArchive(env, ldesc, OUT) }) + It("transfers ctf with named handler", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("transfer", "components", "--copy-resources", "--transfer-handler", "ocm/standard", ARCH, ARCH, OUT)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +using transfer handler ocm/standard +transferring version "github.com/mandelsoft/test:v1"... +...resource 0 testdata[plainText]... +...resource 1 value[ociImage](ocm/value:v2.0)... +...resource 2 ref[ociImage](ocm/ref:v2.0)... +...adding component version... +1 versions transferred +`)) + + Expect(env.DirExists(OUT)).To(BeTrue()) + CheckComponentInArchive(env, ldesc, OUT) + }) + It("transfers ctf with --closure --lookup", func() { buf := bytes.NewBuffer(nil) Expect(env.CatchOutput(buf).Execute("transfer", "components", "--copy-resources", "--recursive", "--lookup", ARCH, ARCH2, ARCH2, OUT)).To(Succeed()) @@ -303,4 +327,80 @@ transferring version "github.com/mandelsoft/test:v1"... Expect(env.FileExists(OUT)).To(BeTrue()) CheckComponentInArchive(env, ldesc, OUT) }) + + Context("plugin execution", func() { + var plugins TempPluginDir + + BeforeEach(func() { + plugins = Must(ConfigureTestPlugins(env, "testdata/plugins")) + }) + + AfterEach(func() { + plugins.Cleanup() + }) + + It("loads plugin", func() { + registry := plugincacheattr.Get(env) + // Expect(registration.RegisterExtensions(env)).To(Succeed()) + p := registry.Get(PLUGIN) + Expect(p).NotTo(BeNil()) + Expect(p.Error()).To(Equal("")) + }) + + It("transfers with copy", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("transfer", "components", + "--transfer-handler", + fmt.Sprintf("plugin/%s/%s=@testdata/config", PLUGIN, HANDLER), + ARCH, ARCH, OUT)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +using transfer handler plugin/transferplugin/demo +transferring version "github.com/mandelsoft/test:v1"... +...resource 0 testdata[plainText]... +...resource 1 value[ociImage](ocm/value:v2.0)... +...resource 2 ref[ociImage](ocm/ref:v2.0)... +...adding component version... +1 versions transferred +`)) + Expect(env.DirExists(OUT)).To(BeTrue()) + CheckComponentInArchive(env, ldesc, OUT) + }) + + It("transfers with copy with implicitly detected handler", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("transfer", "components", + "--transfer-handler", + fmt.Sprintf("plugin/%s=@testdata/config", PLUGIN), + ARCH, ARCH, OUT)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +using transfer handler plugin/transferplugin +transferring version "github.com/mandelsoft/test:v1"... +...resource 0 testdata[plainText]... +...resource 1 value[ociImage](ocm/value:v2.0)... +...resource 2 ref[ociImage](ocm/ref:v2.0)... +...adding component version... +1 versions transferred +`)) + Expect(env.DirExists(OUT)).To(BeTrue()) + CheckComponentInArchive(env, ldesc, OUT) + }) + + It("transfers without copy", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("transfer", "components", + "--transfer-handler", + fmt.Sprintf("plugin/%s/%s=@testdata/config2", PLUGIN, HANDLER), + ARCH, ARCH, OUT)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +using transfer handler plugin/transferplugin/demo +transferring version "github.com/mandelsoft/test:v1"... +...resource 0 testdata[plainText]... +...adding component version... +1 versions transferred +`)) + Expect(env.DirExists(OUT)).To(BeTrue()) + }) + + }) + }) diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/testdata/config b/cmds/ocm/commands/ocmcmds/components/transfer/testdata/config new file mode 100644 index 0000000000..790de3c902 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/transfer/testdata/config @@ -0,0 +1,5 @@ + +transferRepositories: + types: + ociArtifact: + - alias.alias \ No newline at end of file diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/testdata/config2 b/cmds/ocm/commands/ocmcmds/components/transfer/testdata/config2 new file mode 100644 index 0000000000..a511ae3d2d --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/transfer/testdata/config2 @@ -0,0 +1,4 @@ +transferRepositories: + types: + ociArtifact: + - ghcr.io \ No newline at end of file diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/testdata/plugins/transferplugin b/cmds/ocm/commands/ocmcmds/components/transfer/testdata/plugins/transferplugin new file mode 100755 index 0000000000..a07d800501 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/transfer/testdata/plugins/transferplugin @@ -0,0 +1,2 @@ +#!/bin/bash +go run ../../../../../transferplugin/main.go "$@" \ No newline at end of file diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/upload_test.go b/cmds/ocm/commands/ocmcmds/components/transfer/upload_test.go index 494742bc50..e266853d0b 100644 --- a/cmds/ocm/commands/ocmcmds/components/transfer/upload_test.go +++ b/cmds/ocm/commands/ocmcmds/components/transfer/upload_test.go @@ -1,3 +1,5 @@ +//go:build unix + package transfer_test import ( diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/utils.go b/cmds/ocm/commands/ocmcmds/components/transfer/utils.go new file mode 100644 index 0000000000..1a0cc2118b --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/transfer/utils.go @@ -0,0 +1,52 @@ +package transfer + +import ( + "fmt" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" + "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/spiff" + "ocm.software/ocm/api/utils/out" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/scriptoption" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/transferhandleroption" + "ocm.software/ocm/cmds/ocm/common/options" +) + +func ValidateHandler(o options.OptionSetProvider) error { + t := transferhandleroption.From(o) + if t.Path != "" { + s := scriptoption.From(o) + if len(s.ScriptData) > 0 { + if t.Path != "ocm/spiff" { + return fmt.Errorf("transfer handler %q not compatible with script option", t.Path) + } + if len(t.Config) > 0 { + return fmt.Errorf("transfer handler %q with config not compatible with script option", t.Path) + } + } + } + return nil +} + +func DetermineTransferHandler(ctx clictx.Context, o options.OptionSetProvider) (transferhandler.TransferHandler, error) { + var ( + thdlr transferhandler.TransferHandler + err error + ) + + opts := options.FindOptions[transferhandler.TransferOption](o) + th := transferhandleroption.From(o) + if th.Path != "" { + out.Outf(ctx, "using transfer handler %s\n", th.Path) + thdlr, err = transferhandler.For(ctx).ByName(ctx, th.Path, opts...) + } else { + transferopts := &spiff.Options{} + transferhandler.From(ctx.ConfigContext(), transferopts) + transferhandler.ApplyOptions(transferopts, opts...) + thdlr, err = spiff.New(transferopts) + } + if err != nil { + return nil, err + } + return thdlr, nil +} diff --git a/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go b/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go index 227c458b11..ede839ef6f 100644 --- a/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go +++ b/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go @@ -9,7 +9,6 @@ import ( "ocm.software/ocm/api/ocm/extensions/repositories/ctf" "ocm.software/ocm/api/ocm/tools/transfer" "ocm.software/ocm/api/ocm/tools/transfer/transferhandler" - "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/spiff" "ocm.software/ocm/api/utils/accessobj" common "ocm.software/ocm/api/utils/misc" "ocm.software/ocm/cmds/ocm/commands/common/options/closureoption" @@ -23,10 +22,11 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/skipupdateoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/srcbyvalueoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/stoponexistingoption" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/transferhandleroption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/uploaderoption" + comptransfer "ocm.software/ocm/cmds/ocm/commands/ocmcmds/components/transfer" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" "ocm.software/ocm/cmds/ocm/commands/verbs" - "ocm.software/ocm/cmds/ocm/common/options" "ocm.software/ocm/cmds/ocm/common/utils" ) @@ -56,6 +56,7 @@ func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { stoponexistingoption.New(), uploaderoption.New(ctx.OCMContext()), scriptoption.New(), + transferhandleroption.New(), )}, utils.Names(Names, names...)...) } @@ -77,7 +78,7 @@ $ ocm transfer ctf ctf.tgz ghcr.io/mandelsoft/components func (o *Command) Complete(args []string) error { o.SourceName = args[0] o.TargetName = args[1] - return nil + return comptransfer.ValidateHandler(o) } func (o *Command) Run() error { @@ -103,14 +104,11 @@ func (o *Command) Run() error { return err } - thdlr, err := spiff.New( - append(options.FindOptions[transferhandler.TransferOption](o), - spiff.Script(scriptoption.From(o).ScriptData), - spiff.ScriptFilesystem(o.FileSystem()), - )...) + thdlr, err := comptransfer.DetermineTransferHandler(o.Context, o) if err != nil { return err } + a := &action{ printer: common.NewPrinter(o.Context.StdOut()), target: target, diff --git a/cmds/transferplugin/app/app.go b/cmds/transferplugin/app/app.go new file mode 100644 index 0000000000..11d13f916c --- /dev/null +++ b/cmds/transferplugin/app/app.go @@ -0,0 +1,31 @@ +package app + +import ( + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds" + "ocm.software/ocm/api/version" + "ocm.software/ocm/cmds/transferplugin/config" + "ocm.software/ocm/cmds/transferplugin/transferhandlers" +) + +func New() (ppi.Plugin, error) { + p := ppi.NewPlugin("transferplugin", version.Get().String()) + + p.SetShort("demo transfer handler plugin") + p.SetLong("plugin providing a transfer handler to enable value transport for dedicated external repositories.") + p.SetConfigParser(config.GetConfig) + + err := p.RegisterTransferHandler(transferhandlers.New()) + if err != nil { + return nil, err + } + return p, nil +} + +func Run(args []string, opts ...cmds.Option) error { + p, err := New() + if err != nil { + return err + } + return cmds.NewPluginCommand(p, opts...).Execute(args) +} diff --git a/cmds/transferplugin/common/const.go b/cmds/transferplugin/common/const.go new file mode 100644 index 0000000000..2a136fb8dd --- /dev/null +++ b/cmds/transferplugin/common/const.go @@ -0,0 +1,3 @@ +package common + +const CONSUMER_TYPE = "demo" diff --git a/cmds/transferplugin/config/config.go b/cmds/transferplugin/config/config.go new file mode 100644 index 0000000000..9fcd1691b7 --- /dev/null +++ b/cmds/transferplugin/config/config.go @@ -0,0 +1,23 @@ +package config + +import ( + "encoding/json" +) + +type Config struct { + TransferRepositories TransferRepositories `json:"transferRepositories"` +} + +type TransferRepositories struct { + Types map[string][]string `json:"types,omitempty"` +} + +func GetConfig(raw json.RawMessage) (interface{}, error) { + var cfg Config + + err := json.Unmarshal(raw, &cfg) + if err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/cmds/transferplugin/main.go b/cmds/transferplugin/main.go new file mode 100644 index 0000000000..866e7049e5 --- /dev/null +++ b/cmds/transferplugin/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "ocm.software/ocm/cmds/transferplugin/app" +) + +func main() { + err := app.Run(os.Args[1:]) + if err != nil { + os.Exit(1) + } +} diff --git a/cmds/transferplugin/transferhandlers/demo.go b/cmds/transferplugin/transferhandlers/demo.go new file mode 100644 index 0000000000..c1a4880243 --- /dev/null +++ b/cmds/transferplugin/transferhandlers/demo.go @@ -0,0 +1,54 @@ +package transferhandlers + +import ( + "slices" + + "github.com/mandelsoft/goutils/optionutils" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/cmds/transferplugin/config" +) + +const ( + NAME = "demo" +) + +func New() ppi.TransferHandler { + h := ppi.NewTransferHandler(NAME, "enable value transport for dedicated external repositories") + + h.RegisterDecision(ppi.NewTransferResourceDecision(`value transport only for dedicated access types and service hosts`, + ForOptions(func(options *ppi.TransferOptions) bool { return optionutils.AsBool(options.ResourcesByValue) }))) + + h.RegisterDecision(ppi.NewTransferSourceDecision(`value transport only for dedicated access types and service hosts`, + ForOptions(func(options *ppi.TransferOptions) bool { return optionutils.AsBool(options.SourcesByValue) }))) + return h +} + +type OptionFunc func(opts *ppi.TransferOptions) bool + +func ForOptions(f OptionFunc) ppi.ArtifactQuestionFunc { + return func(p ppi.Plugin, question *ppi.ArtifactQuestionArguments) (bool, error) { + var cfg *config.Config + + if question.Options.Special == nil { + c, err := p.GetConfig() + if c == nil || err != nil { + return false, err + } + cfg = c.(*config.Config) + } else { + c, err := config.GetConfig(*question.Options.Special) + if err != nil { + return false, err + } + if c != nil { + cfg = c.(*config.Config) + } + } + + if list := cfg.TransferRepositories.Types[question.Artifact.AccessInfo.Kind]; list != nil { + host := question.Artifact.AccessInfo.Host + return slices.Contains(list, host), nil + } + return f(&question.Options), nil + } +} diff --git a/cmds/transferplugin/transferhandlers/demo_test.go b/cmds/transferplugin/transferhandlers/demo_test.go new file mode 100644 index 0000000000..cf8d0282d4 --- /dev/null +++ b/cmds/transferplugin/transferhandlers/demo_test.go @@ -0,0 +1,91 @@ +package transferhandlers_test + +import ( + "bytes" + "encoding/json" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/goutils/sliceutils" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/ocm" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + v2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "ocm.software/ocm/api/ocm/plugin/common" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/cmds/transferplugin/app" +) + +var _ = Describe("Test Environment", func() { + It("runs a demo question", func() { + var stdout bytes.Buffer + var stderr bytes.Buffer + + config := Must(vfs.ReadFile(osfs.OsFs, "testdata/config")) + + question := &ppi.ArtifactQuestionArguments{ + Source: ppi.SourceComponentVersion{ + Name: "", + Version: "", + Provider: metav1.Provider{}, + Repository: ocm.GenericRepositorySpec{}, + Labels: nil, + }, + Artifact: ppi.Artifact{ + Meta: v2.ElementMeta{}, + Access: ocm.GenericAccessSpec{}, + AccessInfo: ppi.AccessInfo{ + Kind: ociartifact.Type, + Host: "ghcr.io", + Port: "", + Path: "", + Info: "", + }, + }, + Options: ppi.TransferOptions{}, + } + + in := Must(json.Marshal(question)) + + app.Run(sliceutils.AsSlice("--config", string(config), "transferhandler", "demo", ppi.Q_TRANSFER_RESOURCE), cmds.StdIn(bytes.NewBuffer(in)), cmds.StdOut(&stdout), cmds.StdErr(&stderr)) + Expect(stdout.Bytes()).To(YAMLEqual(` +decision: true +`)) + }) + + It("handles empty list", func() { + b := ppi.NewDecisionHandlerBase("x", "") + Expect(b.GetLabels()).NotTo(BeNil()) + }) + + It("describes plugin", func() { + d := Must(app.New()).Descriptor() + p, out := misc.NewBufferedPrinter() + common.DescribePluginDescriptorCapabilities(nil, &d, p) + Expect(out.String()).To(StringEqualTrimmedWithContext(utils.Crop(` + Capabilities: Transfer Handlers + Description: + plugin providing a transfer handler to enable value transport for dedicated external repositories. + + Transfer Handlers: + - Name: demo + enable value transport for dedicated external repositories + Questions: + - Name: transferresource (value transport for resources) + value transport only for dedicated access types and service hosts + consumes no labels + - Name: transfersource (value transport for sources) + value transport only for dedicated access types and service hosts + consumes no labels + +`, 2))) + }) +}) diff --git a/cmds/transferplugin/transferhandlers/suite_test.go b/cmds/transferplugin/transferhandlers/suite_test.go new file mode 100644 index 0000000000..523a8af7f4 --- /dev/null +++ b/cmds/transferplugin/transferhandlers/suite_test.go @@ -0,0 +1,13 @@ +package transferhandlers_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "transfer handler plugin Test Suite") +} diff --git a/cmds/transferplugin/transferhandlers/testdata/config b/cmds/transferplugin/transferhandlers/testdata/config new file mode 100644 index 0000000000..338b2bcaab --- /dev/null +++ b/cmds/transferplugin/transferhandlers/testdata/config @@ -0,0 +1,5 @@ + +transferRepositories: + types: + ociArtifact: + - ghcr.io \ No newline at end of file diff --git a/docs/pluginreference/plugin.md b/docs/pluginreference/plugin.md index dcbad48539..67aa4eac42 100644 --- a/docs/pluginreference/plugin.md +++ b/docs/pluginreference/plugin.md @@ -62,6 +62,7 @@ capabilities of the plugin. * [plugin describe](plugin_describe.md) — describe plugin * [plugin download](plugin_download.md) — download blob into filesystem * [plugin info](plugin_info.md) — show plugin descriptor +* [plugin transferhandler](plugin_transferhandler.md) — decide on a question related to a component version transport * [plugin upload](plugin_upload.md) — upload specific operations * [plugin valuemergehandler](plugin_valuemergehandler.md) — value merge handler operations * [plugin valueset](plugin_valueset.md) — valueset operations diff --git a/docs/pluginreference/plugin_transferhandler.md b/docs/pluginreference/plugin_transferhandler.md new file mode 100644 index 0000000000..77463e85d3 --- /dev/null +++ b/docs/pluginreference/plugin_transferhandler.md @@ -0,0 +1,52 @@ +## plugin transferhandler — Decide On A Question Related To A Component Version Transport + +### Synopsis + +```bash +plugin transferhandler [] +``` + +### Options + +```text + -h, --help help for transferhandler +``` + +### Description + +The task of this command is to decide on questions related to the transport +of component versions, their resources and sources. + +This command is only used for actions for which the transfer handler descriptor +enables the particular question. + +The question arguments are passed as JSON structure on *stdin*, +the result has to be returned as JSON document +on *stdout*. + +There are several questions a handler can answer: +- transferversion: This action answers the question, whether + a component version shall be transported at all and how it should be + transported. The argument is a ComponentVersionQuestion + and the result decision is extended by an optional transport context + consisting, of a new transport handler description and transport options. +- enforcetransport: This action answers the question, whether + a component version shall be transported as if it is not yet present + in the target repository. The argument is a ComponentVersionQuestion. +- updateversion: Update non-signature relevant information. + The argument is a ComponentVersionQuestion. +- overwriteversion: Override signature-relevant information. + The argument is a ComponentVersionQuestion. +- transferresource: Transport resource as value. The argument + is an ArtifactQuestion. +- transfersource: Transport source as value. The argument + is an ArtifactQuestion. + +For detailed types see ocm.software/ocm/api/ocm/plugin/ppi/questions.go. + +### SEE ALSO + +#### Parents + +* [plugin](plugin.md) — OCM Plugin + diff --git a/docs/pluginreference/plugin_valueset_compose.md b/docs/pluginreference/plugin_valueset_compose.md index 9a4a0f0894..f325f17423 100644 --- a/docs/pluginreference/plugin_valueset_compose.md +++ b/docs/pluginreference/plugin_valueset_compose.md @@ -21,7 +21,10 @@ The finally composed set has to be returned as JSON document on *stdout*. This command is only used, if for a value set descriptor configuration -na direct composition rules are configured ([plugin descriptor](plugin_descriptor.md)). +no direct composition rules are configured ([plugin descriptor](plugin_descriptor.md)). + +The purpose describes the purpose the values set is used for: +- routingslip a value used for a routing slip entry. If possible, predefined standard options should be used. In such a case only the name field should be defined for an option. If required, new options can be diff --git a/docs/reference/ocm_transfer_commontransportarchive.md b/docs/reference/ocm_transfer_commontransportarchive.md index 799b4d1d35..196b37b61e 100644 --- a/docs/reference/ocm_transfer_commontransportarchive.md +++ b/docs/reference/ocm_transfer_commontransportarchive.md @@ -28,6 +28,7 @@ commontransportarchive, ctf --script string config name of transfer handler script -s, --scriptFile string filename of transfer handler script -E, --stop-on-existing stop on existing component version in target repository + -T, --transfer-handler string transfer handler ([=) -t, --type string archive format (directory, tar, tgz) (default "directory") --uploader = repository uploader ([:[:[:]]]=) (default []) ``` @@ -181,6 +182,25 @@ Only one of the fields path or script can be used. If no script option is given and the cli config defines a script default this one is used. + +It is possible to use dedicated transfer handlers, either built-in ones or +plugin based handlers. The option --transferHandler can be used to specify +this handler using the hierarchical handler notation scheme. +The following transfer handler names are supported: + - ocm/spiff: spiff transfer handler + + The spiff transfer handler works on the standard transfer options + extended by dynamic programming based on spiff++. + + - ocm/standard: standard transfer handler + + The standard transfer handler works on the standard transfer options. + + - plugin: [transfer handlers provided by plugins] + + sub namespace of the form <plugin name>/<handler> + + ### Examples ```bash diff --git a/docs/reference/ocm_transfer_componentversions.md b/docs/reference/ocm_transfer_componentversions.md index 1c235f0e82..5dd82eddec 100644 --- a/docs/reference/ocm_transfer_componentversions.md +++ b/docs/reference/ocm_transfer_componentversions.md @@ -33,6 +33,7 @@ componentversions, componentversion, cv, components, component, comps, comp, c --script string config name of transfer handler script -s, --scriptFile string filename of transfer handler script -E, --stop-on-existing stop on existing component version in target repository + -T, --transfer-handler string transfer handler ([=) -t, --type string archive format (directory, tar, tgz) (default "directory") --uploader = repository uploader ([:[:[:]]]=) (default []) ``` @@ -238,6 +239,25 @@ Only one of the fields path or script can be used. If no script option is given and the cli config defines a script default this one is used. + +It is possible to use dedicated transfer handlers, either built-in ones or +plugin based handlers. The option --transferHandler can be used to specify +this handler using the hierarchical handler notation scheme. +The following transfer handler names are supported: + - ocm/spiff: spiff transfer handler + + The spiff transfer handler works on the standard transfer options + extended by dynamic programming based on spiff++. + + - ocm/standard: standard transfer handler + + The standard transfer handler works on the standard transfer options. + + - plugin: [transfer handlers provided by plugins] + + sub namespace of the form <plugin name>/<handler> + + ### Examples ```bash