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/credentials/cpi/repotypes.go b/api/credentials/cpi/repotypes.go index 673998c36e..e76e10fcfa 100644 --- a/api/credentials/cpi/repotypes.go +++ b/api/credentials/cpi/repotypes.go @@ -25,15 +25,15 @@ func RegisterRepositoryTypeVersions(s RepositoryTypeVersionScheme) { //////////////////////////////////////////////////////////////////////////////// func NewRepositoryType[I RepositorySpec](name string, opts ...RepositoryOption) RepositoryType { - return descriptivetype.NewTypedObjectTypeObject(runtime.NewVersionedTypedObjectType[RepositorySpec, I](name), opts...) + return descriptivetype.NewVersionedTypedObjectTypeObject(runtime.NewVersionedTypedObjectType[RepositorySpec, I](name), opts...) } func NewRepositoryTypeByConverter[I RepositorySpec, V runtime.TypedObject](name string, converter runtime.Converter[I, V], opts ...RepositoryOption) RepositoryType { - return descriptivetype.NewTypedObjectTypeObject(runtime.NewVersionedTypedObjectTypeByConverter[RepositorySpec, I](name, converter), opts...) + return descriptivetype.NewVersionedTypedObjectTypeObject(runtime.NewVersionedTypedObjectTypeByConverter[RepositorySpec, I](name, converter), opts...) } func NewRepositoryTypeByFormatVersion(name string, fmt runtime.FormatVersion[RepositorySpec], opts ...RepositoryOption) RepositoryType { - return descriptivetype.NewTypedObjectTypeObject(runtime.NewVersionedTypedObjectTypeByFormatVersion[RepositorySpec](name, fmt), opts...) + return descriptivetype.NewVersionedTypedObjectTypeObject(runtime.NewVersionedTypedObjectTypeByFormatVersion[RepositorySpec](name, fmt), opts...) } //////////////////////////////////////////////////////////////////////////////// diff --git a/api/credentials/internal/repository.go b/api/credentials/internal/repository.go index 337d2f4bc8..3f0a306d2b 100644 --- a/api/credentials/internal/repository.go +++ b/api/credentials/internal/repository.go @@ -3,7 +3,7 @@ package internal import ( "github.com/mandelsoft/goutils/set" - common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/misc" ) type Repository interface { @@ -17,16 +17,16 @@ type Credentials interface { ExistsProperty(name string) bool GetProperty(name string) string PropertyNames() set.Set[string] - Properties() common.Properties + Properties() misc.Properties } -type DirectCredentials common.Properties +type DirectCredentials misc.Properties var _ Credentials = (*DirectCredentials)(nil) -func NewCredentials(props common.Properties) DirectCredentials { +func NewCredentials(props misc.Properties) DirectCredentials { if props == nil { - props = common.Properties{} + props = misc.Properties{} } else { props = props.Copy() } @@ -43,11 +43,11 @@ func (c DirectCredentials) GetProperty(name string) string { } func (c DirectCredentials) PropertyNames() set.Set[string] { - return common.Properties(c).Names() + return misc.Properties(c).Names() } -func (c DirectCredentials) Properties() common.Properties { - return common.Properties(c).Copy() +func (c DirectCredentials) Properties() misc.Properties { + return misc.Properties(c).Copy() } func (c DirectCredentials) Credentials(Context, ...CredentialsSource) (Credentials, error) { @@ -55,9 +55,9 @@ func (c DirectCredentials) Credentials(Context, ...CredentialsSource) (Credentia } func (c DirectCredentials) Copy() DirectCredentials { - return DirectCredentials(common.Properties(c).Copy()) + return DirectCredentials(misc.Properties(c).Copy()) } func (c DirectCredentials) String() string { - return common.Properties(c).String() + return misc.Properties(c).String() } diff --git a/api/credentials/internal/repotypes.go b/api/credentials/internal/repotypes.go index dafe999f32..3d2ddad17a 100644 --- a/api/credentials/internal/repotypes.go +++ b/api/credentials/internal/repotypes.go @@ -13,7 +13,7 @@ import ( ) type RepositoryType interface { - descriptivetype.TypedObjectType[RepositorySpec] + descriptivetype.VersionedTypedObjectType[RepositorySpec] } type RepositorySpec interface { @@ -28,22 +28,22 @@ type ( ) type RepositoryTypeScheme interface { - descriptivetype.TypeScheme[RepositorySpec, RepositoryType] + descriptivetype.VersionedTypeScheme[RepositorySpec, RepositoryType] } -type _Scheme = descriptivetype.TypeScheme[RepositorySpec, RepositoryType] +type _Scheme = descriptivetype.VersionedTypeScheme[RepositorySpec, RepositoryType] type repositoryTypeScheme struct { _Scheme } func NewRepositoryTypeScheme(defaultDecoder RepositorySpecDecoder, base ...RepositoryTypeScheme) RepositoryTypeScheme { - scheme := descriptivetype.MustNewDefaultTypeScheme[RepositorySpec, RepositoryType, RepositoryTypeScheme]("Credential provider", nil, &UnknownRepositorySpec{}, true, defaultDecoder, utils.Optional(base...)) + scheme := descriptivetype.MustNewDefaultVersionedTypeScheme[RepositorySpec, RepositoryType, RepositoryTypeScheme]("Credential provider", nil, &UnknownRepositorySpec{}, true, defaultDecoder, utils.Optional(base...)) return &repositoryTypeScheme{scheme} } func NewStrictRepositoryTypeScheme(base ...RepositoryTypeScheme) runtime.VersionedTypeRegistry[RepositorySpec, RepositoryType] { - scheme := descriptivetype.MustNewDefaultTypeScheme[RepositorySpec, RepositoryType, RepositoryTypeScheme]("Credential provider", nil, nil, false, nil, utils.Optional(base...)) + scheme := descriptivetype.MustNewDefaultVersionedTypeScheme[RepositorySpec, RepositoryType, RepositoryTypeScheme]("Credential provider", nil, nil, false, nil, utils.Optional(base...)) return &repositoryTypeScheme{scheme} } 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/accesstypes.go b/api/ocm/cpi/accspeccpi/accesstypes.go index e71fcc9b84..e9a954d2f0 100644 --- a/api/ocm/cpi/accspeccpi/accesstypes.go +++ b/api/ocm/cpi/accspeccpi/accesstypes.go @@ -32,13 +32,13 @@ func MustNewAccessSpecMultiFormatVersion(kind string, formats AccessSpecFormatVe } func NewAccessSpecType[I AccessSpec](name string, opts ...AccessSpecTypeOption) AccessType { - return flagsetscheme.NewTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectType[AccessSpec, I](name), opts...) + return flagsetscheme.NewVersionedTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectType[AccessSpec, I](name), opts...) } func NewAccessSpecTypeByConverter[I AccessSpec, V runtime.VersionedTypedObject](name string, converter runtime.Converter[I, V], opts ...AccessSpecTypeOption) AccessType { - return flagsetscheme.NewTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectTypeByConverter[AccessSpec, I, V](name, converter), opts...) + return flagsetscheme.NewVersionedTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectTypeByConverter[AccessSpec, I, V](name, converter), opts...) } func NewAccessSpecTypeByFormatVersion(name string, fmt runtime.FormatVersion[AccessSpec], opts ...AccessSpecTypeOption) AccessType { - return flagsetscheme.NewTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectTypeByFormatVersion[AccessSpec](name, fmt), opts...) + return flagsetscheme.NewVersionedTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectTypeByFormatVersion[AccessSpec](name, fmt), opts...) } 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/options/deprecated.go b/api/ocm/extensions/accessmethods/options/deprecated.go new file mode 100644 index 0000000000..c175f6b3a7 --- /dev/null +++ b/api/ocm/extensions/accessmethods/options/deprecated.go @@ -0,0 +1,97 @@ +package options + +import ( + "ocm.software/ocm/api/utils/cobrautils/flagsets" +) + +const ( + // Deprecated: use package [flagsets]. + TYPE_STRING = flagsets.TYPE_STRING + // Deprecated: use package [flagsets]. + TYPE_STRINGARRAY = flagsets.TYPE_STRINGARRAY + // Deprecated: use package [flagsets]. + TYPE_STRING2STRING = flagsets.TYPE_STRING2STRING + // Deprecated: use package [flagsets]. + TYPE_INT = flagsets.TYPE_INT + // Deprecated: use package [flagsets]. + TYPE_BOOL = flagsets.TYPE_BOOL + // Deprecated: use package [flagsets]. + TYPE_YAML = flagsets.TYPE_YAML + // Deprecated: use package [flagsets]. + TYPE_STRINGMAPYAML = flagsets.TYPE_STRINGMAPYAML + // Deprecated: use package [flagsets]. + TYPE_STRING2YAML = flagsets.TYPE_STRING2YAML + // Deprecated: use package [flagsets]. + TYPE_STRING2STRINGSLICE = flagsets.TYPE_STRING2STRINGSLICE + // Deprecated: use package [flagsets]. + TYPE_STRINGCOLONSTRINGSLICE = flagsets.TYPE_STRINGCOLONSTRINGSLICE + // Deprecated: use package [flagsets]. + TYPE_BYTES = flagsets.TYPE_BYTES + // Deprecated: use package [flagsets]. + TYPE_IDENTITYPATH = flagsets.TYPE_IDENTITYPATH +) + +// Deprecated: use packagge [flagsets]. +type OptionType = flagsets.ConfigOptionType + +//////////////////////////////////////////////////////////////////////////////// + +// Deprecated: use packagge [flagsets]. +func NewStringOptionType(name, desc string) OptionType { + return flagsets.NewStringOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewStringArrayOptionType(name, desc string) OptionType { + return flagsets.NewStringArrayOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewIntOptionType(name, desc string) OptionType { + return flagsets.NewIntOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewBoolOptionType(name, desc string) OptionType { + return flagsets.NewBoolOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewYAMLOptionType(name, desc string) OptionType { + return flagsets.NewYAMLOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewValueMapYAMLOptionType(name, desc string) OptionType { + return flagsets.NewValueMapYAMLOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewValueMapOptionType(name, desc string) OptionType { + return flagsets.NewValueMapOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewStringMapOptionType(name, desc string) OptionType { + return flagsets.NewStringMapOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewStringSliceMapOptionType(name, desc string) OptionType { + return flagsets.NewStringSliceMapOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewStringSliceMapColonOptionType(name, desc string) OptionType { + return flagsets.NewStringSliceMapColonOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewBytesOptionType(name, desc string) OptionType { + return flagsets.NewBytesOptionType(name, desc) +} + +// Deprecated: use packagge [flagsets]. +func NewIdentityPathOptionType(name, desc string) OptionType { + return flagsets.NewIdentityPathOptionType(name, desc) +} diff --git a/api/ocm/extensions/accessmethods/options/init.go b/api/ocm/extensions/accessmethods/options/init.go index 7e815b8f30..f0ff625e01 100644 --- a/api/ocm/extensions/accessmethods/options/init.go +++ b/api/ocm/extensions/accessmethods/options/init.go @@ -1,34 +1,10 @@ package options -const ( - TYPE_STRING = "string" - TYPE_STRINGARRAY = "[]string" - TYPE_STRING2STRING = "string=string" - TYPE_INT = "int" - TYPE_BOOL = "bool" - TYPE_YAML = "YAML" - TYPE_STRINGMAPYAML = "map[string]YAML" - TYPE_STRING2YAML = "string=YAML" - TYPE_STRING2STRINGSLICE = "string=string,string" - TYPE_STRINGCOLONSTRINGSLICE = "string:string,string" - TYPE_BYTES = "[]byte" - TYPE_IDENTITYPATH = "[]identity" +import ( + "ocm.software/ocm/api/utils/cobrautils/flagsets" ) -func init() { - DefaultRegistry.RegisterValueType(TYPE_STRING, NewStringOptionType, "string value") - DefaultRegistry.RegisterValueType(TYPE_STRINGARRAY, NewStringArrayOptionType, "list of string values") - DefaultRegistry.RegisterValueType(TYPE_STRING2STRING, NewStringMapOptionType, "string map defined by dedicated assignments") - DefaultRegistry.RegisterValueType(TYPE_INT, NewIntOptionType, "integer value") - DefaultRegistry.RegisterValueType(TYPE_BOOL, NewBoolOptionType, "boolean flag") - DefaultRegistry.RegisterValueType(TYPE_YAML, NewYAMLOptionType, "JSON or YAML document string") - DefaultRegistry.RegisterValueType(TYPE_STRINGMAPYAML, NewValueMapYAMLOptionType, "JSON or YAML map") - DefaultRegistry.RegisterValueType(TYPE_STRING2YAML, NewValueMapOptionType, "string map with arbitrary values defined by dedicated assignments") - DefaultRegistry.RegisterValueType(TYPE_STRING2STRINGSLICE, NewStringSliceMapOptionType, "string map defined by dedicated assignment of comma separated strings") - DefaultRegistry.RegisterValueType(TYPE_STRINGCOLONSTRINGSLICE, NewStringSliceMapColonOptionType, "string map defined by dedicated assignment of comma separated strings") - DefaultRegistry.RegisterValueType(TYPE_BYTES, NewBytesOptionType, "byte value") - DefaultRegistry.RegisterValueType(TYPE_IDENTITYPATH, NewIdentityPathOptionType, "identity path") -} +var DefaultRegistry = flagsets.SetBaseTypes(flagsets.NewConfigOptionTypeRegistry()) func RegisterOption(o OptionType) OptionType { DefaultRegistry.RegisterOptionType(o) diff --git a/api/ocm/extensions/accessmethods/options/registry_test.go b/api/ocm/extensions/accessmethods/options/registry_test.go deleted file mode 100644 index 675823db0f..0000000000 --- a/api/ocm/extensions/accessmethods/options/registry_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package options - -import ( - . "github.com/mandelsoft/goutils/testutils" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("registry", func() { - var reg Registry - - BeforeEach(func() { - reg = New() - }) - - It("sets and retrieves type", func() { - reg.RegisterValueType("string", NewStringOptionType, "string") - - t := reg.GetValueType("string") - Expect(t).NotTo(BeNil()) - - o := Must(reg.CreateOptionType("string", "test", "some test")) - Expect(o.GetName()).To(Equal("test")) - Expect(o.GetDescription()).To(Equal("[*string*] some test")) - }) - - It("sets and retrieves option", func() { - reg.RegisterOptionType(HostnameOption) - - t := reg.GetOptionType(HostnameOption.GetName()) - Expect(t).NotTo(BeNil()) - }) - - It("creates merges a new type", func() { - reg.RegisterValueType("string", NewStringOptionType, "string") - reg.RegisterOptionType(HostnameOption) - - o := Must(reg.CreateOptionType("string", HostnameOption.GetName(), "some test")) - Expect(o).To(BeIdenticalTo(HostnameOption)) - }) - - It("fails creating existing", func() { - reg.RegisterValueType("string", NewStringOptionType, "string") - reg.RegisterValueType("int", NewIntOptionType, "int") - reg.RegisterOptionType(HostnameOption) - - _, err := reg.CreateOptionType("int", HostnameOption.GetName(), "some test") - MustFailWithMessage(err, "option \"accessHostname\" already exists") - }) -}) diff --git a/api/ocm/extensions/accessmethods/options/standard.go b/api/ocm/extensions/accessmethods/options/standard.go index a2b6ccb99b..0ebefbbd7d 100644 --- a/api/ocm/extensions/accessmethods/options/standard.go +++ b/api/ocm/extensions/accessmethods/options/standard.go @@ -1,84 +1,88 @@ package options +import ( + "ocm.software/ocm/api/utils/cobrautils/flagsets" +) + // HintOption . -var HintOption = RegisterOption(NewStringOptionType("hint", "(repository) hint for local artifacts")) +var HintOption = RegisterOption(flagsets.NewStringOptionType("hint", "(repository) hint for local artifacts")) // MediatypeOption . -var MediatypeOption = RegisterOption(NewStringOptionType("mediaType", "media type for artifact blob representation")) +var MediatypeOption = RegisterOption(flagsets.NewStringOptionType("mediaType", "media type for artifact blob representation")) // SizeOption . -var SizeOption = RegisterOption(NewIntOptionType("size", "blob size")) +var SizeOption = RegisterOption(flagsets.NewIntOptionType("size", "blob size")) // DigestOption . -var DigestOption = RegisterOption(NewStringOptionType("digest", "blob digest")) +var DigestOption = RegisterOption(flagsets.NewStringOptionType("digest", "blob digest")) // ReferenceOption . -var ReferenceOption = RegisterOption(NewStringOptionType("reference", "reference name")) +var ReferenceOption = RegisterOption(flagsets.NewStringOptionType("reference", "reference name")) // PackageOption . -var PackageOption = RegisterOption(NewStringOptionType("package", "package or object name")) +var PackageOption = RegisterOption(flagsets.NewStringOptionType("package", "package or object name")) // ArtifactOption . -var ArtifactOption = RegisterOption(NewStringOptionType("artifactId", "maven artifact id")) +var ArtifactOption = RegisterOption(flagsets.NewStringOptionType("artifactId", "maven artifact id")) // GroupOption . -var GroupOption = RegisterOption(NewStringOptionType("groupId", "maven group id")) +var GroupOption = RegisterOption(flagsets.NewStringOptionType("groupId", "maven group id")) // RepositoryOption . -var RepositoryOption = RegisterOption(NewStringOptionType("accessRepository", "repository or registry URL")) +var RepositoryOption = RegisterOption(flagsets.NewStringOptionType("accessRepository", "repository or registry URL")) // HostnameOption . -var HostnameOption = RegisterOption(NewStringOptionType("accessHostname", "hostname used for access")) +var HostnameOption = RegisterOption(flagsets.NewStringOptionType("accessHostname", "hostname used for access")) // CommitOption . -var CommitOption = RegisterOption(NewStringOptionType("commit", "git commit id")) +var CommitOption = RegisterOption(flagsets.NewStringOptionType("commit", "git commit id")) // GlobalAccessOption . -var GlobalAccessOption = RegisterOption(NewValueMapYAMLOptionType("globalAccess", "access specification for global access")) +var GlobalAccessOption = RegisterOption(flagsets.NewValueMapYAMLOptionType("globalAccess", "access specification for global access")) // RegionOption . -var RegionOption = RegisterOption(NewStringOptionType("region", "region name")) +var RegionOption = RegisterOption(flagsets.NewStringOptionType("region", "region name")) // BucketOption . -var BucketOption = RegisterOption(NewStringOptionType("bucket", "bucket name")) +var BucketOption = RegisterOption(flagsets.NewStringOptionType("bucket", "bucket name")) // VersionOption . -var VersionOption = RegisterOption(NewStringOptionType("accessVersion", "version for access specification")) +var VersionOption = RegisterOption(flagsets.NewStringOptionType("accessVersion", "version for access specification")) // ComponentOption. -var ComponentOption = RegisterOption(NewStringOptionType("accessComponent", "component for access specification")) +var ComponentOption = RegisterOption(flagsets.NewStringOptionType("accessComponent", "component for access specification")) // IdentityPathOption. -var IdentityPathOption = RegisterOption(NewIdentityPathOptionType("identityPath", "identity path for specification")) +var IdentityPathOption = RegisterOption(flagsets.NewIdentityPathOptionType("identityPath", "identity path for specification")) // URLOption. -var URLOption = RegisterOption(NewStringOptionType("url", "artifact or server url")) +var URLOption = RegisterOption(flagsets.NewStringOptionType("url", "artifact or server url")) -var HTTPHeaderOption = RegisterOption(NewStringSliceMapColonOptionType("header", "http headers")) +var HTTPHeaderOption = RegisterOption(flagsets.NewStringSliceMapColonOptionType("header", "http headers")) -var HTTPVerbOption = RegisterOption(NewStringOptionType("verb", "http request method")) +var HTTPVerbOption = RegisterOption(flagsets.NewStringOptionType("verb", "http request method")) -var HTTPBodyOption = RegisterOption(NewStringOptionType("body", "body of a http request")) +var HTTPBodyOption = RegisterOption(flagsets.NewStringOptionType("body", "body of a http request")) -var HTTPRedirectOption = RegisterOption(NewBoolOptionType("noredirect", "http redirect behavior")) +var HTTPRedirectOption = RegisterOption(flagsets.NewBoolOptionType("noredirect", "http redirect behavior")) // CommentOption . -var CommentOption = RegisterOption(NewStringOptionType("comment", "comment field value")) +var CommentOption = RegisterOption(flagsets.NewStringOptionType("comment", "comment field value")) // ClassifierOption the optional classifier of a maven resource. -var ClassifierOption = RegisterOption(NewStringOptionType("classifier", "maven classifier")) +var ClassifierOption = RegisterOption(flagsets.NewStringOptionType("classifier", "maven classifier")) // ExtensionOption the optional extension of a maven resource. -var ExtensionOption = RegisterOption(NewStringOptionType("extension", "maven extension name")) +var ExtensionOption = RegisterOption(flagsets.NewStringOptionType("extension", "maven extension name")) // NPMRegistryOption sets the registry of the npm resource. -var NPMRegistryOption = RegisterOption(NewStringOptionType("registry", "npm package registry")) +var NPMRegistryOption = RegisterOption(flagsets.NewStringOptionType("registry", "npm package registry")) // NPMPackageOption sets what package should be fetched from the npm registry. -var NPMPackageOption = RegisterOption(NewStringOptionType("package", "npm package name")) +var NPMPackageOption = PackageOption // NPMVersionOption sets the version of the npm package. -var NPMVersionOption = RegisterOption(NewStringOptionType("version", "npm package version")) +var NPMVersionOption = RegisterOption(flagsets.NewStringOptionType("version", "npm package version")) // IdPathOption is a path of identity specs. -var IdPathOption = RegisterOption(NewStringArrayOptionType("idpath", "identity path (attr=value{,attr=value}")) +var IdPathOption = RegisterOption(flagsets.NewStringArrayOptionType("idpath", "identity path (attr=value{,attr=value}")) diff --git a/api/ocm/extensions/accessmethods/options/types.go b/api/ocm/extensions/accessmethods/options/types.go deleted file mode 100644 index f02ecd03f7..0000000000 --- a/api/ocm/extensions/accessmethods/options/types.go +++ /dev/null @@ -1,125 +0,0 @@ -package options - -import ( - "fmt" - - "ocm.software/ocm/api/utils/cobrautils/flagsets" -) - -type OptionType interface { - flagsets.ConfigOptionType - ValueType() string - GetDescriptionText() string -} - -type base = flagsets.ConfigOptionType - -type option struct { - base - valueType string -} - -func (o *option) Equal(t flagsets.ConfigOptionType) bool { - if ot, ok := t.(*option); ok { - return o.valueType == ot.valueType && o.GetName() == ot.GetName() - } - return false -} - -func (o *option) ValueType() string { - return o.valueType -} - -func (o *option) GetDescription() string { - return fmt.Sprintf("[*%s*] %s", o.ValueType(), o.base.GetDescription()) -} - -func (o *option) GetDescriptionText() string { - return o.base.GetDescription() -} - -//////////////////////////////////////////////////////////////////////////////// - -func NewStringOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewStringOptionType(name, desc), - valueType: TYPE_STRING, - } -} - -func NewStringArrayOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewStringArrayOptionType(name, desc), - valueType: TYPE_STRINGARRAY, - } -} - -func NewIntOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewIntOptionType(name, desc), - valueType: TYPE_INT, - } -} - -func NewBoolOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewBoolOptionType(name, desc), - valueType: TYPE_BOOL, - } -} - -func NewYAMLOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewYAMLOptionType(name, desc), - valueType: TYPE_YAML, - } -} - -func NewValueMapYAMLOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewValueMapYAMLOptionType(name, desc), - valueType: TYPE_STRINGMAPYAML, - } -} - -func NewValueMapOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewValueMapOptionType(name, desc), - valueType: TYPE_STRING2YAML, - } -} - -func NewStringMapOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewStringMapOptionType(name, desc), - valueType: TYPE_STRING2STRING, - } -} - -func NewStringSliceMapOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewStringSliceMapOptionType(name, desc), - valueType: TYPE_STRING2STRINGSLICE, - } -} - -func NewStringSliceMapColonOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewStringSliceMapColonOptionType(name, desc), - valueType: TYPE_STRINGCOLONSTRINGSLICE, - } -} - -func NewBytesOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewBytesOptionType(name, desc), - valueType: TYPE_BYTES, - } -} - -func NewIdentityPathOptionType(name, desc string) OptionType { - return &option{ - base: flagsets.NewIdentityPathOptionType(name, desc), - valueType: TYPE_IDENTITYPATH, - } -} 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..5e70200d57 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,23 +94,45 @@ 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 "" } - return info.Short + return info.MediaType } func (p *PluginHandler) GetReferenceHint(spec *AccessSpec, cv cpi.ComponentVersionAccess) string { @@ -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/extensions/labels/routingslip/internal/entrytypes.go b/api/ocm/extensions/labels/routingslip/internal/entrytypes.go index dbb7f42051..f70f325b58 100644 --- a/api/ocm/extensions/labels/routingslip/internal/entrytypes.go +++ b/api/ocm/extensions/labels/routingslip/internal/entrytypes.go @@ -41,18 +41,18 @@ type ( //////////////////////////////////////////////////////////////////////////////// -type EntryTypeScheme = flagsetscheme.ExtendedTypeScheme[Entry, EntryType, flagsets.ExplicitlyTypedConfigTypeOptionSetConfigProvider] +type EntryTypeScheme = flagsetscheme.ExtendedProviderTypeScheme[Entry, EntryType, flagsets.ExplicitlyTypedConfigTypeOptionSetConfigProvider] -func unwrapTypeScheme(s EntryTypeScheme) flagsetscheme.TypeScheme[Entry, EntryType] { +func unwrapTypeScheme(s EntryTypeScheme) flagsetscheme.VersionedTypeScheme[Entry, EntryType] { return s.Unwrap() } func NewEntryTypeScheme(base ...EntryTypeScheme) EntryTypeScheme { - return flagsetscheme.NewTypeSchemeWrapper[Entry, EntryType, flagsets.ExplicitlyTypedConfigTypeOptionSetConfigProvider](flagsetscheme.NewTypeScheme[Entry, EntryType, flagsetscheme.TypeScheme[Entry, EntryType]]("Entry type", "entry", "", "routing slip entry specification", "Entry Specification Options", &UnknownEntry{}, true, sliceutils.Transform(base, unwrapTypeScheme)...)) + return flagsetscheme.NewVersionedTypeSchemeWrapper[Entry, EntryType, flagsets.ExplicitlyTypedConfigTypeOptionSetConfigProvider](flagsetscheme.NewVersionedTypeScheme[Entry, EntryType, flagsetscheme.VersionedTypeScheme[Entry, EntryType]]("Entry type", "entry", "", "routing slip entry specification", "Entry Specification Options", &UnknownEntry{}, true, sliceutils.Transform(base, unwrapTypeScheme)...)) } func NewStrictEntryTypeScheme(base ...EntryTypeScheme) EntryTypeScheme { - return flagsetscheme.NewTypeSchemeWrapper[Entry, EntryType, flagsets.ExplicitlyTypedConfigTypeOptionSetConfigProvider](flagsetscheme.NewTypeScheme[Entry, EntryType, flagsetscheme.TypeScheme[Entry, EntryType]]("Entry type", "entry", "", "routing slip entry specification", "Entry Specification Options", nil, false, sliceutils.Transform(base, unwrapTypeScheme)...)) + return flagsetscheme.NewVersionedTypeSchemeWrapper[Entry, EntryType, flagsets.ExplicitlyTypedConfigTypeOptionSetConfigProvider](flagsetscheme.NewVersionedTypeScheme[Entry, EntryType, flagsetscheme.VersionedTypeScheme[Entry, EntryType]]("Entry type", "entry", "", "routing slip entry specification", "Entry Specification Options", nil, false, sliceutils.Transform(base, unwrapTypeScheme)...)) } func CreateEntry(t runtime.TypedObject) (Entry, error) { diff --git a/api/ocm/extensions/labels/routingslip/spi/support.go b/api/ocm/extensions/labels/routingslip/spi/support.go index b0e50ca240..4dc95f32c4 100644 --- a/api/ocm/extensions/labels/routingslip/spi/support.go +++ b/api/ocm/extensions/labels/routingslip/spi/support.go @@ -26,15 +26,15 @@ func MustNewEntryMultiFormatVersion(kind string, formats EntryFormatVersionRegis //////////////////////////////////////////////////////////////////////////////// func NewEntryType[I Entry](name string, opts ...EntryTypeOption) EntryType { - return flagsetscheme.NewTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectType[Entry, I](name), opts...) + return flagsetscheme.NewVersionedTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectType[Entry, I](name), opts...) } func NewEntryTypeByConverter[I Entry, V runtime.VersionedTypedObject](name string, converter runtime.Converter[I, V], opts ...EntryTypeOption) EntryType { - return flagsetscheme.NewTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectTypeByConverter[Entry, I, V](name, converter), opts...) + return flagsetscheme.NewVersionedTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectTypeByConverter[Entry, I, V](name, converter), opts...) } func NewEntryTypeByFormatVersion(name string, fmt runtime.FormatVersion[Entry], opts ...EntryTypeOption) EntryType { - return flagsetscheme.NewTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectTypeByFormatVersion[Entry](name, fmt), opts...) + return flagsetscheme.NewVersionedTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectTypeByFormatVersion[Entry](name, fmt), opts...) } //////////////////////////////////////////////////////////////////////////////// diff --git a/api/ocm/extensions/pubsub/interface.go b/api/ocm/extensions/pubsub/interface.go index 744a14a82a..f7fc402f14 100644 --- a/api/ocm/extensions/pubsub/interface.go +++ b/api/ocm/extensions/pubsub/interface.go @@ -9,7 +9,6 @@ import ( "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/goutils/general" "github.com/mandelsoft/goutils/generics" - "github.com/mandelsoft/goutils/optionutils" "github.com/modern-go/reflect2" "ocm.software/ocm/api/ocm/cpi" @@ -33,7 +32,7 @@ func WithDesciption(desc string) Option { //////////////////////////////////////////////////////////////////////////////// -type PubSubType descriptivetype.TypedObjectType[PubSubSpec] +type PubSubType descriptivetype.VersionedTypedObjectType[PubSubSpec] // PubSubSpec is the interface publish/subscribe specifications // must fulfill. The main task is to map the specification @@ -61,14 +60,14 @@ type PubSubMethod interface { // PubSub types. A PubSub type is finally able to // provide an implementation for notifying a dedicated // PubSub instance. -type TypeScheme descriptivetype.TypeScheme[PubSubSpec, PubSubType] +type TypeScheme descriptivetype.VersionedTypeScheme[PubSubSpec, PubSubType] func NewTypeScheme(base ...TypeScheme) TypeScheme { - return descriptivetype.NewTypeScheme[PubSubSpec, PubSubType, TypeScheme]("PubSub type", nil, &UnknownPubSubSpec{}, false, base...) + return descriptivetype.NewVersionedTypeScheme[PubSubSpec, PubSubType, TypeScheme]("PubSub type", nil, &UnknownPubSubSpec{}, false, base...) } func NewStrictTypeScheme(base ...TypeScheme) runtime.VersionedTypeRegistry[PubSubSpec, PubSubType] { - return descriptivetype.NewTypeScheme[PubSubSpec, PubSubType, TypeScheme]("PubSub type", nil, &UnknownPubSubSpec{}, false, base...) + return descriptivetype.NewVersionedTypeScheme[PubSubSpec, PubSubType, TypeScheme]("PubSub type", nil, &UnknownPubSubSpec{}, false, base...) } // DefaultTypeScheme contains all globally known PubSub serializers. @@ -83,10 +82,7 @@ func CreatePubSubSpec(t runtime.TypedObject) (PubSubSpec, error) { } func NewPubSubType[I PubSubSpec](name string, opts ...Option) PubSubType { - t := descriptivetype.NewTypedObjectTypeObject[PubSubSpec](runtime.NewVersionedTypedObjectType[PubSubSpec, I](name)) - ta := descriptivetype.NewTypeObjectTarget[PubSubSpec](t) - optionutils.ApplyOptions[descriptivetype.OptionTarget](ta, opts...) - return t + return descriptivetype.NewVersionedTypedObjectTypeObject[PubSubSpec](runtime.NewVersionedTypedObjectType[PubSubSpec, I](name), opts...) } //////////////////////////////////////////////////////////////////////////////// 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..8b1c861882 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 @@ -102,14 +106,14 @@ type AccessMethod interface { AsBlobAccess() BlobAccess } -type AccessTypeScheme flagsetscheme.TypeScheme[AccessSpec, AccessType] +type AccessTypeScheme flagsetscheme.VersionedTypeScheme[AccessSpec, AccessType] func NewAccessTypeScheme(base ...AccessTypeScheme) AccessTypeScheme { - return flagsetscheme.NewTypeScheme[AccessSpec, AccessType, AccessTypeScheme]("Access type", "access", "accessType", "blob access specification", "Access Specification Options", &UnknownAccessSpec{}, true, base...) + return flagsetscheme.NewVersionedTypeScheme[AccessSpec, AccessType, AccessTypeScheme]("Access type", "access", "accessType", "blob access specification", "Access Specification Options", &UnknownAccessSpec{}, true, base...) } func NewStrictAccessTypeScheme(base ...AccessTypeScheme) runtime.VersionedTypeRegistry[AccessSpec, AccessType] { - return flagsetscheme.NewTypeScheme[AccessSpec, AccessType, AccessTypeScheme]("Access type", "access", "accessType", "blob access specification", "Access Specification Options", nil, false, base...) + return flagsetscheme.NewVersionedTypeScheme[AccessSpec, AccessType, AccessTypeScheme]("Access type", "access", "accessType", "blob access specification", "Access Specification Options", nil, false, base...) } // DefaultAccessTypeScheme contains all globally known access serializer. @@ -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..69d85918d7 100644 --- a/api/ocm/plugin/cache/plugin.go +++ b/api/ocm/plugin/cache/plugin.go @@ -131,6 +131,30 @@ func (p *pluginImpl) GetLabelMergeSpecification(name, version string) *descripto return fallback } +func (p *pluginImpl) GetInputTypeDescriptor(name, version string) *descriptor.InputTypeDescriptor { + if !p.IsValid() { + return nil + } + + var fallback descriptor.InputTypeDescriptor + fallbackFound := false + for _, i := range p.descriptor.Inputs { + if i.Name == name { + if i.Version == version { + return &i + } + if i.Version == "" || i.Version == "v1" { + fallback = i + fallbackFound = true + } + } + } + if fallbackFound && (version == "" || version == "v1") { + return &fallback + } + return nil +} + func (p *pluginImpl) GetAccessMethodDescriptor(name, version string) *descriptor.AccessMethodDescriptor { if !p.IsValid() { return nil @@ -232,6 +256,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..09586658f8 100644 --- a/api/ocm/plugin/common/describe.go +++ b/api/ocm/plugin/common/describe.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/Masterminds/semver/v3" + "github.com/mandelsoft/goutils/generics" + "github.com/mandelsoft/goutils/maputils" "github.com/mandelsoft/goutils/set" "golang.org/x/exp/slices" @@ -12,6 +14,7 @@ import ( "ocm.software/ocm/api/ocm/extensions/accessmethods/options" "ocm.software/ocm/api/ocm/plugin/descriptor" "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/cobrautils/flagsets" common "ocm.software/ocm/api/utils/misc" "ocm.software/ocm/api/utils/semverutils" ) @@ -80,6 +83,16 @@ 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) + } + if len(d.SigningHandlers) > 0 { + out.Printf("\n") + out.Printf("Signing Handlers:\n") + DescribeSigningHandlers(d, out) + } } type MethodInfo struct { @@ -91,7 +104,7 @@ type MethodInfo struct { type MethodVersion struct { Name string Format string - Options map[string]options.OptionType + Options map[string]flagsets.ConfigOptionType } func GetAccessMethodInfo(methods []descriptor.AccessMethodDescriptor) map[string]*MethodInfo { @@ -117,7 +130,7 @@ func GetAccessMethodInfo(methods []descriptor.AccessMethodDescriptor) map[string if v == nil { v = &MethodVersion{ Name: vers, - Options: map[string]options.OptionType{}, + Options: map[string]flagsets.ConfigOptionType{}, } i.Versions[vers] = v } @@ -316,7 +329,7 @@ type ValueSetInfo struct { type ValueSetVersion struct { Name string Format string - Options map[string]options.OptionType + Options map[string]flagsets.ConfigOptionType } func GetValueSetInfo(valuesets []descriptor.ValueSetDescriptor) map[string]*ValueSetInfo { @@ -345,7 +358,7 @@ func GetValueSetInfo(valuesets []descriptor.ValueSetDescriptor) map[string]*Valu if v == nil { v = &ValueSetVersion{ Name: vers, - Options: map[string]options.OptionType{}, + Options: map[string]flagsets.ConfigOptionType{}, } i.Versions[vers] = v } @@ -517,6 +530,102 @@ 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) + } + } + } + } + } +} + +func GetSigningInfo(types []descriptor.SigningHandlerDescriptor) map[string]*descriptor.SigningHandlerDescriptor { + found := map[string]*descriptor.SigningHandlerDescriptor{} + for _, m := range types { + i := found[m.Name] + if i == nil { + found[m.Name] = generics.Pointer(m) + } + } + return found +} + +func DescribeSigningHandlers(d *descriptor.Descriptor, out common.Printer) { + handlers := GetSigningInfo(d.SigningHandlers) + for _, n := range maputils.OrderedKeys(handlers) { + out.Printf("- Name: %s\n", n) + m := handlers[n] + if m.Description != "" { + out.Printf("%s\n", utils.IndentLines(m.Description, " ")) + } + out := out.AddGap(" ") + out.Printf("Credentials: %T\n", m.Credentials) + out.Printf("Signer: %T\n", m.Signer) + out.Printf("Verifier: %T\n", m.Verifier) + } +} + +//////////////////////////////////////////////////////////////////////////////// + +func GetName[T descriptor.Named](n T) string { + return n.GetName() +} + 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..54fc87e456 100644 --- a/api/ocm/plugin/descriptor/const.go +++ b/api/ocm/plugin/descriptor/const.go @@ -7,13 +7,17 @@ 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_INPUTTYPE = "input type" + KIND_ACCESSMETHOD = errkind.KIND_ACCESSMETHOD + KIND_ACTION = action.KIND_ACTION + KIND_TRANSFERHANDLER = "transferhandler" + KIND_QUESTION = "question" + KIND_SIGNING_HANDLER = "signing handler" + 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..d4a293203a 100644 --- a/api/ocm/plugin/descriptor/descriptor.go +++ b/api/ocm/plugin/descriptor/descriptor.go @@ -17,6 +17,7 @@ type Descriptor struct { ForwardLogging bool `json:"forwardLogging"` Actions []ActionDescriptor `json:"actions,omitempty"` + Inputs []InputTypeDescriptor `json:"inputs,omitempty"` AccessMethods []AccessMethodDescriptor `json:"accessMethods,omitempty"` Uploaders List[UploaderDescriptor] `json:"uploaders,omitempty"` Downloaders List[DownloaderDescriptor] `json:"downloaders,omitempty"` @@ -24,6 +25,8 @@ 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"` + SigningHandlers List[SigningHandlerDescriptor] `json:"signingHandlers,omitempty"` ConfigTypes List[ConfigTypeDescriptor] `json:"configTypes,omitempty"` } @@ -58,6 +61,12 @@ 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") + } + if len(d.SigningHandlers) > 0 { + caps = append(caps, "Signing Handlers") + } return caps } @@ -117,12 +126,20 @@ func (d UploaderDescriptor) GetConstraints() []UploaderKey { return d.Constraints } +//////////////////////////////////////////////////////////////////////////////// + type AccessMethodDescriptor struct { ValueSetDefinition `json:",inline"` } //////////////////////////////////////////////////////////////////////////////// +type InputTypeDescriptor struct { + ValueSetDefinition `json:",inline"` +} + +//////////////////////////////////////////////////////////////////////////////// + type ValueTypeDefinition struct { Name string `json:"name"` Version string `json:"version,omitempty"` @@ -235,6 +252,54 @@ 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 SigningHandlerDescriptor struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Credentials bool `json:"credentials,omitempty"` + Signer bool `json:"signer,omitempty"` + Verifier bool `json:"verifier,omitempty"` +} + +func (d SigningHandlerDescriptor) GetName() string { + return d.Name +} + +//////////////////////////////////////////////////////////////////////////////// + type ConfigTypeDescriptor = ValueTypeDefinition //////////////////////////////////////////////////////////////////////////////// diff --git a/api/ocm/plugin/interface.go b/api/ocm/plugin/interface.go index 4d4091179e..ce2278495c 100644 --- a/api/ocm/plugin/interface.go +++ b/api/ocm/plugin/interface.go @@ -3,13 +3,16 @@ package plugin import ( "ocm.software/ocm/api/ocm/plugin/descriptor" "ocm.software/ocm/api/ocm/plugin/internal" + "ocm.software/ocm/api/tech/signing" ) 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_INPUTTYPE = descriptor.KIND_INPUTTYPE + KIND_ACTION = descriptor.KIND_ACTION + KIND_TRANSFERHANDLER = descriptor.KIND_TRANSFERHANDLER ) var TAG = descriptor.REALM @@ -18,6 +21,7 @@ type ( Descriptor = descriptor.Descriptor ActionDescriptor = descriptor.ActionDescriptor ValueMergeHandlerDescriptor = descriptor.ValueMergeHandlerDescriptor + InputTypeDescriptor = descriptor.InputTypeDescriptor AccessMethodDescriptor = descriptor.AccessMethodDescriptor DownloaderDescriptor = descriptor.DownloaderDescriptor DownloaderKey = descriptor.DownloaderKey @@ -29,5 +33,40 @@ type ( CommandDescriptor = descriptor.CommandDescriptor AccessSpecInfo = internal.AccessSpecInfo + InputSpecInfo = internal.InputSpecInfo UploadTargetSpecInfo = internal.UploadTargetSpecInfo + + SignatureSpec = internal.SignatureSpec +) + +func SignatureSpecFor(sig *signing.Signature) *SignatureSpec { + return internal.SignatureSpecFor(sig) +} + +// +// 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/input.go b/api/ocm/plugin/internal/input.go new file mode 100644 index 0000000000..a2b3b0cbbb --- /dev/null +++ b/api/ocm/plugin/internal/input.go @@ -0,0 +1,12 @@ +package internal + +import ( + "ocm.software/ocm/api/credentials" +) + +type InputSpecInfo struct { + Short string `json:"short"` + MediaType string `json:"mediaType"` + Hint string `json:"hint"` + ConsumerId credentials.ConsumerIdentity `json:"consumerId"` +} diff --git a/api/ocm/plugin/internal/signing.go b/api/ocm/plugin/internal/signing.go new file mode 100644 index 0000000000..495f987132 --- /dev/null +++ b/api/ocm/plugin/internal/signing.go @@ -0,0 +1,141 @@ +package internal + +import ( + "crypto" + "crypto/x509/pkix" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/generics" + + "ocm.software/ocm/api/credentials" + v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/tech/signing" + "ocm.software/ocm/api/tech/signing/signutils" +) + +type ( + SignatureSpec = v1.SignatureSpec +) + +func SignatureSpecFor(sig *signing.Signature) *SignatureSpec { + return v1.SignatureSpecFor(sig) +} + +type SigningContext struct { + HashAlgo crypto.Hash + PrivateKey signutils.GenericPrivateKey + PublicKey signutils.GenericPublicKey + Issuer *pkix.Name + + Credentials credentials.DirectCredentials +} + +var ( + _ json.Unmarshaler = (*SigningContext)(nil) + _ json.Marshaler = (*SigningContext)(nil) +) + +type signingContext struct { + HashAlgo string `json:"hashAlgorithm"` + PrivateKey string `json:"privatekey,omitempty"` + PublicKey string `json:"publicKey,omitempty"` + Issuer string `json:"issuer,omitempty"` + Credentials credentials.DirectCredentials `json:"credentials,omitempty"` +} + +func (s SigningContext) MarshalJSON() ([]byte, error) { + var err error + + ser := &signingContext{ + HashAlgo: s.HashAlgo.String(), + } + + if s.PrivateKey != nil { + ser.PrivateKey, err = encode(s.PrivateKey, signutils.GetPrivateKey, signutils.PemBlockForPrivateKey) + if err != nil { + return nil, err + } + } + if s.PublicKey != nil { + ser.PublicKey, err = encode(s.PublicKey, signutils.GetPublicKey, func(in interface{}) *pem.Block { return signutils.PemBlockForPublicKey(in) }) + if err != nil { + return nil, err + } + } + if s.Issuer != nil { + ser.Issuer = signutils.DNAsString(*s.Issuer) + } + if s.Credentials != nil { + ser.Credentials = s.Credentials + } + return json.Marshal(ser) +} + +func (s *SigningContext) UnmarshalJSON(bytes []byte) error { + var ser signingContext + + err := json.Unmarshal(bytes, &ser) + if err != nil { + return err + } + + h := signing.DefaultRegistry().GetHasher(ser.HashAlgo) + if h == nil { + return errors.ErrUnknown(signutils.KIND_HASH_ALGORITHM, ser.HashAlgo) + } + s.HashAlgo = h.Crypto() + if ser.Issuer != "" { + s.Issuer, err = signutils.ParseDN(ser.Issuer) + if err != nil { + return err + } + } + + s.PrivateKey, err = decode[signutils.GenericPrivateKey](ser.PrivateKey, signutils.ParsePrivateKey) + if err != nil { + return err + } + s.PublicKey, err = decode[signutils.GenericPublicKey](ser.PublicKey, signutils.ParsePublicKey) + if err != nil { + return err + } + s.Credentials = ser.Credentials + return nil +} + +func encode[T any](in T, f func(in T) (interface{}, error), e func(in interface{}) *pem.Block) (string, error) { + k, err := f(in) + if err != nil { + var i interface{} = in + switch d := i.(type) { + case []byte: + return hex.EncodeToString(d), nil + case string: + return hex.EncodeToString([]byte(d)), nil + default: + return "", fmt.Errorf("invalid key type") + } + } + b := e(k) + if b == nil { + return "", fmt.Errorf("cannot encode key") + } + return string(pem.EncodeToMemory(b)), nil +} + +func decode[T any](in string, f func([]byte) (interface{}, error)) (T, error) { + var _nil T + k, err := f([]byte(in)) + if err != nil { + data, err := hex.DecodeString(in) + if err != nil { + return _nil, err + } + k = data + } + return generics.Cast[T](k), nil +} 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..c4a171ebba 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/descriptor" "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,11 @@ 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" + signingcmd "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing/consumer" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing/sign" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing/verify" + "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" @@ -36,6 +42,7 @@ import ( vscompose "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset/compose" vsval "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset/validate" "ocm.software/ocm/api/ocm/valuemergehandler" + "ocm.software/ocm/api/tech/signing" "ocm.software/ocm/api/utils/cobrautils/flagsets" "ocm.software/ocm/api/utils/cobrautils/logopts/logging" "ocm.software/ocm/api/utils/runtime" @@ -443,3 +450,108 @@ 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 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) +} + +//////////////////////////////////////////////////////////////////////////////// + +func (p *pluginImpl) GetSigningConsumer(name string, sctx signing.SigningContext) (credentials.ConsumerIdentity, error) { + sh := p.GetDescriptor().SigningHandlers.Get(name) + if sh == nil { + return nil, errors.ErrUnknown(descriptor.KIND_SIGNING_HANDLER, name) + } + if !sh.Credentials { + return nil, nil + } + + ser := ppi.SigningContext{ + HashAlgo: sctx.GetHash(), + PrivateKey: sctx.GetPrivateKey(), + PublicKey: sctx.GetPublicKey(), + Issuer: sctx.GetIssuer(), + } + indata, err := json.Marshal(ser) + if err != nil { + return nil, errors.Wrapf(err, "cannot marshal signing context") + } + + result, err := p.Exec(bytes.NewReader(indata), nil, signingcmd.Name, consumer.Name, name) + if err != nil { + return nil, err + } + var r credentials.ConsumerIdentity + err = json.Unmarshal(result, &r) + if err != nil { + return nil, errors.Wrapf(err, "cannot unmarshal signing result") + } + + return r, nil +} + +func (p *pluginImpl) Sign(name string, digest string, creds credentials.DirectCredentials, sctx signing.SigningContext) (*signing.Signature, error) { + ser := ppi.SigningContext{ + HashAlgo: sctx.GetHash(), + PrivateKey: sctx.GetPrivateKey(), + PublicKey: sctx.GetPublicKey(), + Issuer: sctx.GetIssuer(), + + Credentials: creds, + } + indata, err := json.Marshal(ser) + if err != nil { + return nil, errors.Wrapf(err, "cannot marshal signing context") + } + + result, err := p.Exec(bytes.NewReader(indata), nil, signingcmd.Name, sign.Name, name, digest) + if err != nil { + return nil, err + } + var r SignatureSpec + err = json.Unmarshal(result, &r) + if err != nil { + return nil, errors.Wrapf(err, "cannot unmarshal signing result") + } + + return r.ConvertToSigning(), nil +} + +func (p *pluginImpl) Verify(name string, digest string, signature *signing.Signature, sctx signing.SigningContext) error { + ser := ppi.SigningContext{ + HashAlgo: sctx.GetHash(), + PrivateKey: sctx.GetPrivateKey(), + PublicKey: sctx.GetPublicKey(), + Issuer: sctx.GetIssuer(), + } + indata, err := json.Marshal(ser) + if err != nil { + return errors.Wrapf(err, "cannot marshal signing context") + } + + sig := SignatureSpecFor(signature) + sigdata, err := json.Marshal(sig) + if err != nil { + return errors.Wrapf(err, "cannot marshal signature") + } + _, err = p.Exec(bytes.NewReader(indata), nil, signingcmd.Name, verify.Name, name, digest, string(sigdata)) + return err +} diff --git a/api/ocm/plugin/plugin_input.go b/api/ocm/plugin/plugin_input.go new file mode 100644 index 0000000000..4b04df5a27 --- /dev/null +++ b/api/ocm/plugin/plugin_input.go @@ -0,0 +1,70 @@ +package plugin + +import ( + "encoding/json" + "io" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input/compose" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input/get" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input/validate" + "ocm.software/ocm/api/utils/cobrautils/flagsets" +) + +func (p *pluginImpl) ValidateInputSpec(spec []byte) (*ppi.InputSpecInfo, error) { + result, err := p.Exec(nil, nil, input.Name, validate.Name, string(spec)) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s", p.Name()) + } + + var info ppi.InputSpecInfo + err = json.Unmarshal(result, &info) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s: cannot unmarshal input spec info", p.Name()) + } + return &info, nil +} + +func (p *pluginImpl) ComposeInputSpec(name string, opts flagsets.ConfigOptions, base flagsets.Config) error { + cfg := flagsets.Config{} + for _, o := range opts.Options() { + cfg[o.GetName()] = o.Value() + } + optsdata, err := json.Marshal(cfg) + if err != nil { + return errors.Wrapf(err, "cannot marshal option values") + } + basedata, err := json.Marshal(base) + if err != nil { + return errors.Wrapf(err, "cannot marshal input specification base value") + } + result, err := p.Exec(nil, nil, input.Name, compose.Name, name, string(optsdata), string(basedata)) + if err != nil { + return err + } + var r flagsets.Config + err = json.Unmarshal(result, &r) + if err != nil { + return errors.Wrapf(err, "cannot unmarshal composition result") + } + + for k := range base { + delete(base, k) + } + for k, v := range r { + base[k] = v + } + return nil +} + +func (p *pluginImpl) GetInputBlob(w io.Writer, dir string, creds, spec json.RawMessage) error { + args := []string{input.Name, get.Name, dir, string(spec)} + if creds != nil { + args = append(args, "--"+get.OptCreds, string(creds)) + } + _, err := p.Exec(nil, w, args...) + return err +} 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..1f700b00b6 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" @@ -15,8 +16,11 @@ import ( "ocm.software/ocm/api/ocm/plugin/ppi/cmds/describe" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/download" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/info" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input" "ocm.software/ocm/api/ocm/plugin/ppi/cmds/mergehandler" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing" "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 +35,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() @@ -62,10 +66,13 @@ func NewPluginCommand(p ppi.Plugin) *PluginCommand { cmd.AddCommand(action.New(p)) cmd.AddCommand(mergehandler.New(p)) cmd.AddCommand(accessmethod.New(p)) + cmd.AddCommand(input.New(p)) cmd.AddCommand(upload.New(p)) cmd.AddCommand(download.New(p)) cmd.AddCommand(valueset.New(p)) cmd.AddCommand(command.New(p)) + cmd.AddCommand(transferhandler.New(p)) + cmd.AddCommand(signing.New(p)) cmd.InitDefaultHelpCmd() help := cobrautils.GetHelpCommand(cmd) @@ -76,8 +83,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 +103,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/input/cmd.go b/api/ocm/plugin/ppi/cmds/input/cmd.go new file mode 100644 index 0000000000..444d84f508 --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/input/cmd.go @@ -0,0 +1,26 @@ +package input + +import ( + "github.com/spf13/cobra" + + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input/compose" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input/get" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/input/validate" +) + +const Name = "input" + +func New(p ppi.Plugin) *cobra.Command { + cmd := &cobra.Command{ + Use: Name, + Short: "input operations for component version composition", + Long: `This command group provides all commands used to implement an input type +described by an input type descriptor (` + p.Name() + ` descriptor.`, + } + + cmd.AddCommand(validate.New(p)) + cmd.AddCommand(get.New(p)) + cmd.AddCommand(compose.New(p)) + return cmd +} diff --git a/api/ocm/plugin/ppi/cmds/input/compose/cmd.go b/api/ocm/plugin/ppi/cmds/input/compose/cmd.go new file mode 100644 index 0000000000..5add87613f --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/input/compose/cmd.go @@ -0,0 +1,93 @@ +package compose + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/options" +) + +const Name = "compose" + +func New(p ppi.Plugin) *cobra.Command { + opts := Options{} + + cmd := &cobra.Command{ + Use: Name + " ", + Short: "compose input specification from options and base specification", + Long: ` +The task of this command is to compose an input specification based on some +explicitly given input options and preconfigured specifications. + +The finally composed input specification has to be returned as JSON document +on *stdout*. + +This command is only used, if for an input method descriptor configuration +options are defined (` + p.Name() + ` descriptor). + +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 +defined by additionally specifying a type and a description. New options should +be used very carefully. The chosen names MUST not conflict with names provided +by other plugins. Therefore, it is highly recommended to use names prefixed +by the plugin name. + +` + options.DefaultRegistry.Usage(), + Args: cobra.ExactArgs(3), + 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 { + Name string + Options ppi.Config + Base ppi.Config +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { +} + +func (o *Options) Complete(args []string) error { + o.Name = args[0] + if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[1]), &o.Options); err != nil { + return errors.Wrapf(err, "invalid input specification options") + } + if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[2]), &o.Base); err != nil { + return errors.Wrapf(err, "invalid base input specification") + } + return nil +} + +func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { + m := p.GetInputType(opts.Name) + if m == nil { + return errors.ErrUnknown(descriptor.KIND_INPUTTYPE, opts.Name) + } + err := opts.Options.ConvertFor(m.Options()...) + if err != nil { + return err + } + err = m.ComposeSpecification(p, opts.Options, opts.Base) + if err != nil { + return err + } + data, err := json.Marshal(opts.Base) + if err != nil { + return err + } + cmd.Printf("%s\n", string(data)) + return nil +} diff --git a/api/ocm/plugin/ppi/cmds/input/get/cmd.go b/api/ocm/plugin/ppi/cmds/input/get/cmd.go new file mode 100644 index 0000000000..d310a32786 --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/input/get/cmd.go @@ -0,0 +1,95 @@ +package get + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi" + commonppi "ocm.software/ocm/api/ocm/plugin/ppi/cmds/common" + "ocm.software/ocm/api/utils/cobrautils/flag" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + Name = "get" + OptCreds = commonppi.OptCreds +) + +func New(p ppi.Plugin) *cobra.Command { + opts := Options{} + + cmd := &cobra.Command{ + Use: Name + " [] [] ", + Short: "get blob", + Long: ` +Evaluate the given input specification and return the described blob on +*stdout*.`, + Args: cobra.RangeArgs(1, 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 { + Credentials credentials.DirectCredentials + Specification json.RawMessage + Dir string +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { + flag.YAMLVarP(fs, &o.Credentials, OptCreds, "c", nil, "credentials") + flag.StringToStringVarPFA(fs, &o.Credentials, "credential", "C", nil, "dedicated credential value") +} + +func (o *Options) Complete(args []string) error { + creds := 0 + if len(args) > 1 { + o.Dir = args[creds] + creds++ + } else { + o.Dir = "." + } + if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[creds]), &o.Specification); err != nil { + return errors.Wrapf(err, "invalid repository specification") + } + + fmt.Fprintf(os.Stderr, "credentials: %s\n", o.Credentials.String()) + return nil +} + +func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { + spec, err := p.DecodeInputSpecification(opts.Specification) + if err != nil { + return errors.Wrapf(err, "access specification") + } + + m := p.GetInputType(spec.GetType()) + if m == nil { + return errors.ErrUnknown(descriptor.KIND_INPUTTYPE, spec.GetType()) + } + _, err = m.ValidateSpecification(p, opts.Dir, spec) + if err != nil { + return err + } + r, err := m.Reader(p, opts.Dir, spec, opts.Credentials) + if err != nil { + return err + } + _, err = io.Copy(os.Stdout, r) + r.Close() + return err +} diff --git a/api/ocm/plugin/ppi/cmds/input/validate/cmd.go b/api/ocm/plugin/ppi/cmds/input/validate/cmd.go new file mode 100644 index 0000000000..60a08c787d --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/input/validate/cmd.go @@ -0,0 +1,113 @@ +package validate + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/utils/runtime" +) + +const Name = "validate" + +func New(p ppi.Plugin) *cobra.Command { + opts := Options{} + + cmd := &cobra.Command{ + Use: Name + " ", + Short: "validate input specification", + Long: ` +This command accepts an input specification as argument. It is used to +validate the specification and to provide some metadata for the given +specification. + +This metadata has to be provided as JSON string on *stdout* and has the +following fields: + +- **mediaType** *string* + + The media type of the artifact described by the specification. It may be part + of the specification or implicitly determined by the input type. + +- **description** *string* + + A short textual description of the described location. + +- **hint** *string* + + A name hint of the described location used to reconstruct a useful + name for local blobs uploaded to a dedicated repository technology. + +- **consumerId** *map[string]string* + + The consumer id used to determine optional credentials for the + underlying repository. If specified, at least the type field must be set. +`, + Args: cobra.RangeArgs(1, 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 { + Specification json.RawMessage + Dir string +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { +} + +func (o *Options) Complete(args []string) error { + creds := 0 + if len(args) > 1 { + o.Dir = args[creds] + creds++ + } else { + o.Dir = "." + } + if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[creds]), &o.Specification); err != nil { + return errors.Wrapf(err, "invalid access specification") + } + return nil +} + +type Result struct { + MediaType string `json:"mediaType"` + Short string `json:"description"` + Hint string `json:"hint"` + ConsumerId credentials.ConsumerIdentity `json:"consumerId"` +} + +func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { + spec, err := p.DecodeInputSpecification(opts.Specification) + if err != nil { + return errors.Wrapf(err, "input specification") + } + + m := p.GetInputType(spec.GetType()) + if m == nil { + return errors.ErrUnknown(descriptor.KIND_INPUTTYPE, spec.GetType()) + } + info, err := m.ValidateSpecification(p, opts.Dir, spec) + if err != nil { + return err + } + result := Result{MediaType: info.MediaType, ConsumerId: info.ConsumerId, Hint: info.Hint, Short: info.Short} + data, err := json.Marshal(result) + if err != nil { + return err + } + cmd.Printf("%s\n", string(data)) + return nil +} diff --git a/api/ocm/plugin/ppi/cmds/signing/cmd.go b/api/ocm/plugin/ppi/cmds/signing/cmd.go new file mode 100644 index 0000000000..018abaea66 --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/signing/cmd.go @@ -0,0 +1,26 @@ +package signing + +import ( + "github.com/spf13/cobra" + + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing/consumer" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing/sign" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/signing/verify" +) + +const Name = "signing" + +func New(p ppi.Plugin) *cobra.Command { + cmd := &cobra.Command{ + Use: Name, + Short: "signing related operations", + Long: `This command group provides all commands used for the signing extension +described by signing descriptor (` + p.Name() + ` descriptor).`, + } + + cmd.AddCommand(consumer.New(p)) + cmd.AddCommand(sign.New(p)) + cmd.AddCommand(verify.New(p)) + return cmd +} diff --git a/api/ocm/plugin/ppi/cmds/signing/consumer/cmd.go b/api/ocm/plugin/ppi/cmds/signing/consumer/cmd.go new file mode 100644 index 0000000000..0b7666336f --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/signing/consumer/cmd.go @@ -0,0 +1,91 @@ +package consumer + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/tech/signing" +) + +const ( + Name = "consumer" +) + +func New(p ppi.Plugin) *cobra.Command { + opts := Options{} + + cmd := &cobra.Command{ + Use: Name + " ", + Short: "determine consumer id", + Long: ` +Provide the required credential consumer id for signing context taken from +*stdin*. The id is returned by *stdout*. +`, + Args: cobra.ExactArgs(1), + 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 { + Name string +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { +} + +func (o *Options) Complete(args []string) error { + o.Name = args[0] + return nil +} + +func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { + data, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + + var ctx ppi.SigningContext + err = json.Unmarshal(data, &ctx) + if err != nil { + return err + } + + h := p.GetSigningHandler(opts.Name) + if h == nil { + return errors.ErrUnknown(descriptor.KIND_SIGNING_HANDLER) + } + c := h.GetConsumerProvider() + if c == nil { + return fmt.Errorf("handler %q does not support consumer provider", opts.Name) + } + + sctx := &signing.DefaultSigningContext{ + Hash: ctx.HashAlgo, + PrivateKey: ctx.PrivateKey, + PublicKey: ctx.PublicKey, + Issuer: ctx.Issuer, + } + + cid := c(sctx) + + data, err = json.Marshal(cid) + if err != nil { + return err + } + cmd.OutOrStdout().Write(data) + return nil +} diff --git a/api/ocm/plugin/ppi/cmds/signing/sign/cmd.go b/api/ocm/plugin/ppi/cmds/signing/sign/cmd.go new file mode 100644 index 0000000000..9c1afcdacd --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/signing/sign/cmd.go @@ -0,0 +1,103 @@ +package sign + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/tech/signing" +) + +const ( + Name = "sign" +) + +func New(p ppi.Plugin) *cobra.Command { + opts := Options{} + + cmd := &cobra.Command{ + Use: Name + " ", + Short: "sign digest", + Long: ` +Sign the given digest with signing context taken from *stdin* and return the signature on *stdout*.`, + 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 { + Name string + Digest string +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { +} + +func (o *Options) Complete(args []string) error { + o.Name = args[0] + o.Digest = args[1] + return nil +} + +func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { + data, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + + var ctx ppi.SigningContext + err = json.Unmarshal(data, &ctx) + if err != nil { + return err + } + + h := p.GetSigningHandler(opts.Name) + if h == nil { + return errors.ErrUnknown(descriptor.KIND_SIGNING_HANDLER) + } + s := h.GetSigner() + if s == nil { + return fmt.Errorf("handler %q does not support signing", opts.Name) + } + + sctx := &signing.DefaultSigningContext{ + Hash: ctx.HashAlgo, + PrivateKey: ctx.PrivateKey, + PublicKey: ctx.PublicKey, + Issuer: ctx.Issuer, + } + + cctx := credentials.New(datacontext.MODE_EXTENDED) + if h.GetConsumerProvider() != nil && ctx.Credentials != nil { + cid := h.GetConsumerProvider()(sctx) + cctx.SetCredentialsForConsumer(cid, ctx.Credentials) + } + + sig, err := s.Sign(cctx, opts.Digest, sctx) + if err != nil { + return err + } + + spec := ppi.SignatureSpecFor(sig) + out, err := json.Marshal(spec) + if err != nil { + return err + } + _, err = cmd.OutOrStdout().Write(out) + return err +} diff --git a/api/ocm/plugin/ppi/cmds/signing/verify/cmd.go b/api/ocm/plugin/ppi/cmds/signing/verify/cmd.go new file mode 100644 index 0000000000..0d5761df51 --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/signing/verify/cmd.go @@ -0,0 +1,93 @@ +package verify + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/tech/signing" +) + +const ( + Name = "verify" +) + +func New(p ppi.Plugin) *cobra.Command { + opts := Options{} + + cmd := &cobra.Command{ + Use: Name + " ", + Short: "verify signature", + Long: ` +Verify the given digest and signature with signing context taken from *stdin*.`, + Args: cobra.ExactArgs(3), + 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 { + Name string + Digest string + Signature *signing.Signature +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { +} + +func (o *Options) Complete(args []string) error { + var sig ppi.SignatureSpec + + err := json.Unmarshal([]byte(args[2]), &sig) + if err != nil { + return err + } + + o.Name = args[0] + o.Digest = args[1] + o.Signature = sig.ConvertToSigning() + return nil +} + +func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { + data, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + + var ctx ppi.SigningContext + err = json.Unmarshal(data, &ctx) + if err != nil { + return err + } + + h := p.GetSigningHandler(opts.Name) + if h == nil { + return errors.ErrUnknown(descriptor.KIND_SIGNING_HANDLER) + } + v := h.GetVerifier() + if v == nil { + return fmt.Errorf("handler %q does not support verifying", opts.Name) + } + + sctx := &signing.DefaultSigningContext{ + Hash: ctx.HashAlgo, + PrivateKey: ctx.PrivateKey, + PublicKey: ctx.PublicKey, + Issuer: ctx.Issuer, + } + + return v.Verify(opts.Digest, opts.Signature, sctx) +} 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..eba7fe0906 100644 --- a/api/ocm/plugin/ppi/interface.go +++ b/api/ocm/plugin/ppi/interface.go @@ -9,9 +9,10 @@ import ( "ocm.software/ocm/api/config/cpi" "ocm.software/ocm/api/credentials" "ocm.software/ocm/api/datacontext/action" - "ocm.software/ocm/api/ocm/extensions/accessmethods/options" "ocm.software/ocm/api/ocm/plugin/descriptor" "ocm.software/ocm/api/ocm/plugin/internal" + "ocm.software/ocm/api/tech/signing" + "ocm.software/ocm/api/utils/cobrautils/flagsets" "ocm.software/ocm/api/utils/runtime" ) @@ -26,10 +27,21 @@ type ( ActionSpecInfo = internal.ActionSpecInfo AccessSpecInfo = internal.AccessSpecInfo + InputSpecInfo = internal.InputSpecInfo ValueSetInfo = internal.ValueSetInfo UploadTargetSpecInfo = internal.UploadTargetSpecInfo + + SourceComponentVersion = internal.SourceComponentVersion + TargetRepositorySpec = internal.TargetRepositorySpec + StandardTransferOptions = internal.TransferOptions + + SignatureSpec = internal.SignatureSpec ) +func SignatureSpecFor(sig *signing.Signature) *SignatureSpec { + return internal.SignatureSpecFor(sig) +} + var REALM = descriptor.REALM type Plugin interface { @@ -57,6 +69,10 @@ type Plugin interface { DecodeAccessSpecification(data []byte) (AccessSpec, error) GetAccessMethod(name string, version string) AccessMethod + RegisterInputType(m InputType) error + DecodeInputSpecification(data []byte) (InputSpec, error) + GetInputType(name string) InputType + RegisterAction(a Action) error DecodeAction(data []byte) (ActionSpec, error) GetAction(name string) Action @@ -72,6 +88,14 @@ type Plugin interface { GetCommand(name string) Command Commands() []Command + RegisterTransferHandler(h TransferHandler) error + GetTransferHandler(name string) TransferHandler + TransferHandlers() []TransferHandler + + RegisterSigningHandler(h SigningHandler) error + GetSigningHandler(name string) SigningHandler + SigningHandlers() []SigningHandler + RegisterConfigType(c cpi.ConfigType) error GetConfigType(name string) *descriptor.ConfigTypeDescriptor ConfigTypes() []descriptor.ConfigTypeDescriptor @@ -80,6 +104,31 @@ type Plugin interface { GetConfig() (interface{}, error) } +//////////////////////////////////////////////////////////////////////////////// + +type InputType interface { + runtime.TypedObjectDecoder[InputSpec] + + Name() string + + // Options provides the list of CLI options supported to compose the access + // specification. + Options() []flagsets.ConfigOptionType + + // Description provides a general description for the access mehod kind. + Description() string + // Format describes the attributes of the dedicated version. + Format() string + + ValidateSpecification(p Plugin, dir string, spec InputSpec) (info *InputSpecInfo, err error) + Reader(p Plugin, dir string, spec InputSpec, creds credentials.Credentials) (io.ReadCloser, error) + ComposeSpecification(p Plugin, opts Config, config Config) error +} + +type InputSpec = runtime.TypedObject + +//////////////////////////////////////////////////////////////////////////////// + type AccessMethod interface { runtime.TypedObjectDecoder[AccessSpec] @@ -88,7 +137,7 @@ type AccessMethod interface { // Options provides the list of CLI options supported to compose the access // specification. - Options() []options.OptionType + Options() []flagsets.ConfigOptionType // Description provides a general description for the access mehod kind. Description() string @@ -104,6 +153,8 @@ type AccessSpec = runtime.TypedObject type AccessSpecProvider func() AccessSpec +//////////////////////////////////////////////////////////////////////////////// + type UploadFormats runtime.KnownTypes[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]] type Uploader interface { @@ -118,6 +169,8 @@ type Uploader interface { type UploadTargetSpec = runtime.TypedObject +//////////////////////////////////////////////////////////////////////////////// + type DownloadResultProvider func() (string, error) type Downloader interface { @@ -128,6 +181,8 @@ type Downloader interface { Writer(p Plugin, arttype, mediatype string, filepath string, config []byte) (io.WriteCloser, DownloadResultProvider, error) } +//////////////////////////////////////////////////////////////////////////////// + type ActionSpec = action.ActionSpec type ActionResult = action.ActionResult @@ -141,6 +196,8 @@ type Action interface { Execute(p Plugin, spec ActionSpec, creds credentials.DirectCredentials) (result ActionResult, err error) } +//////////////////////////////////////////////////////////////////////////////// + type Value = runtime.RawValue type ValueMergeResult struct { @@ -173,7 +230,7 @@ type ValueSet interface { // Options provides the list of CLI options supported to compose the access // specification. - Options() []options.OptionType + Options() []flagsets.ConfigOptionType // Description provides a general description for the access mehod kind. Description() string @@ -184,6 +241,8 @@ type ValueSet interface { ComposeSpecification(p Plugin, opts Config, config Config) error } +//////////////////////////////////////////////////////////////////////////////// + // Command is the interface for a CLI command provided by a plugin. type Command interface { // Name of command used in the plugin. @@ -211,3 +270,68 @@ 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 +) + +//////////////////////////////////////////////////////////////////////////////// + +type ( + SigningContext = internal.SigningContext + ConsumerProvider func(sctx signing.SigningContext) credentials.ConsumerIdentity +) + +type SigningHandler interface { + GetName() string + GetDescription() string + GetSigner() signing.Signer + GetVerifier() signing.Verifier + + GetConsumerProvider() ConsumerProvider +} diff --git a/api/ocm/plugin/ppi/plugin.go b/api/ocm/plugin/ppi/plugin.go index 319a2891ea..7b0d5a6c74 100644 --- a/api/ocm/plugin/ppi/plugin.go +++ b/api/ocm/plugin/ppi/plugin.go @@ -18,8 +18,10 @@ import ( "ocm.software/ocm/api/ocm/ocmutils/registry" "ocm.software/ocm/api/ocm/plugin/descriptor" "ocm.software/ocm/api/utils/cobrautils" + "ocm.software/ocm/api/utils/cobrautils/flagsets" "ocm.software/ocm/api/utils/errkind" "ocm.software/ocm/api/utils/runtime" + inpoptions "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/options" ) type plugin struct { @@ -39,6 +41,9 @@ type plugin struct { methods map[string]AccessMethod accessScheme runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]] + inputs map[string]InputType + inputScheme runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]] + actions map[string]Action mergehandlers map[string]ValueMergeHandler mergespecs map[string]*descriptor.LabelMergeSpecification @@ -46,6 +51,10 @@ type plugin struct { valuesets map[string]map[string]ValueSet setScheme map[string]runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]] + transferhandlers map[string]TransferHandler + + signinghandlers map[string]SigningHandler + clicmds map[string]Command configParser func(message json.RawMessage) (interface{}, error) @@ -55,7 +64,9 @@ func NewPlugin(name string, version string) Plugin { return &plugin{ name: name, version: version, + methods: map[string]AccessMethod{}, + inputs: map[string]InputType{}, downloaders: map[string]Downloader{}, downmappings: registry.NewRegistry[Downloader, DownloaderKey](), @@ -63,6 +74,7 @@ func NewPlugin(name string, version string) Plugin { uploaders: map[string]Uploader{}, upmappings: registry.NewRegistry[Uploader, UploaderKey](), + inputScheme: runtime.MustNewDefaultScheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]](&runtime.UnstructuredVersionedTypedObject{}, false, nil), accessScheme: runtime.MustNewDefaultScheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]](&runtime.UnstructuredVersionedTypedObject{}, false, nil), uploaderScheme: runtime.MustNewDefaultScheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]](&runtime.UnstructuredVersionedTypedObject{}, false, nil), @@ -70,6 +82,9 @@ func NewPlugin(name string, version string) Plugin { mergehandlers: map[string]ValueMergeHandler{}, mergespecs: map[string]*descriptor.LabelMergeSpecification{}, + transferhandlers: map[string]TransferHandler{}, + signinghandlers: map[string]SigningHandler{}, + valuesets: map[string]map[string]ValueSet{}, setScheme: map[string]runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]]{}, @@ -278,21 +293,13 @@ func (p *plugin) DecodeUploadTargetSpecification(data []byte) (UploadTargetSpec, //////////////////////////////////////////////////////////////////////////////// -func (p *plugin) RegisterAccessMethod(m AccessMethod) error { - if p.GetAccessMethod(m.Name(), m.Version()) != nil { - n := m.Name() - if m.Version() != "" { - n += runtime.VersionSeparator + m.Version() - } - return errors.ErrAlreadyExists(errkind.KIND_ACCESSMETHOD, n) - } - +func cliOpts(reg flagsets.ConfigOptionTypeRegistry, optTypes []flagsets.ConfigOptionType) ([]CLIOption, error) { var optlist []CLIOption - for _, o := range m.Options() { - known := options.DefaultRegistry.GetOptionType(o.GetName()) + for _, o := range optTypes { + known := reg.GetOptionType(o.GetName()) if known != nil { if o.ValueType() != known.ValueType() { - return fmt.Errorf("option type %s[%s] conflicts with standard option type using value type %s", o.GetName(), o.ValueType(), known.ValueType()) + return nil, fmt.Errorf("option type %s[%s] conflicts with standard option type using value type %s", o.GetName(), o.ValueType(), known.ValueType()) } optlist = append(optlist, CLIOption{ Name: o.GetName(), @@ -301,10 +308,64 @@ func (p *plugin) RegisterAccessMethod(m AccessMethod) error { optlist = append(optlist, CLIOption{ Name: o.GetName(), Type: o.ValueType(), - Description: o.GetDescriptionText(), + Description: o.GetDescription(), }) } } + return optlist, nil +} + +func (p *plugin) RegisterInputType(m InputType) error { + if p.GetInputType(m.Name()) != nil { + n := m.Name() + return errors.ErrAlreadyExists(descriptor.KIND_INPUTTYPE, n) + } + + optlist, err := cliOpts(inpoptions.DefaultRegistry, m.Options()) + if err != nil { + return err + } + + inp := descriptor.InputTypeDescriptor{ + ValueSetDefinition: descriptor.ValueSetDefinition{ + ValueTypeDefinition: descriptor.ValueTypeDefinition{ + Name: m.Name(), + Description: m.Description(), + Format: m.Format(), + }, + CLIOptions: optlist, + }, + } + p.descriptor.Inputs = append(p.descriptor.Inputs, inp) + p.inputScheme.RegisterByDecoder(m.Name(), m) + p.inputs[m.Name()] = m + return nil +} + +func (p *plugin) DecodeInputSpecification(data []byte) (InputSpec, error) { + return p.inputScheme.Decode(data, nil) +} + +func (p *plugin) GetInputType(name string) InputType { + return p.inputs[name] +} + +//////////////////////////////////////////////////////////////////////////////// + +func (p *plugin) RegisterAccessMethod(m AccessMethod) error { + if p.GetAccessMethod(m.Name(), m.Version()) != nil { + n := m.Name() + if m.Version() != "" { + n += runtime.VersionSeparator + m.Version() + } + return errors.ErrAlreadyExists(errkind.KIND_ACCESSMETHOD, n) + } + + optlist, err := cliOpts(options.DefaultRegistry, m.Options()) + if err != nil { + return err + } + vers := m.Version() if vers == "" { meth := descriptor.AccessMethodDescriptor{ @@ -321,6 +382,7 @@ func (p *plugin) RegisterAccessMethod(m AccessMethod) error { p.methods[m.Name()] = m vers = "v1" } + meth := descriptor.AccessMethodDescriptor{ ValueSetDefinition: descriptor.ValueSetDefinition{ ValueTypeDefinition: descriptor.ValueTypeDefinition{ @@ -455,24 +517,11 @@ func (p *plugin) RegisterValueSet(s ValueSet) error { } } - var optlist []CLIOption - for _, o := range s.Options() { - known := options.DefaultRegistry.GetOptionType(o.GetName()) - if known != nil { - if o.ValueType() != known.ValueType() { - return fmt.Errorf("option type %s[%s] conflicts with standard option type using value type %s", o.GetName(), o.ValueType(), known.ValueType()) - } - optlist = append(optlist, CLIOption{ - Name: o.GetName(), - }) - } else { - optlist = append(optlist, CLIOption{ - Name: o.GetName(), - Type: o.ValueType(), - Description: o.GetDescriptionText(), - }) - } + optlist, err := cliOpts(options.DefaultRegistry, s.Options()) + if err != nil { + return err } + vers := s.Version() if vers == "" { set := descriptor.ValueSetDescriptor{ @@ -597,6 +646,64 @@ 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) GetSigningHandler(name string) SigningHandler { + return p.signinghandlers[name] +} + +func (p *plugin) RegisterSigningHandler(h SigningHandler) error { + if p.GetSigningHandler(h.GetName()) != nil { + return errors.ErrAlreadyExists("signing handler", h.GetName()) + } + d := descriptor.SigningHandlerDescriptor{ + Name: h.GetName(), + Description: h.GetDescription(), + Credentials: h.GetConsumerProvider() != nil, + Signer: h.GetSigner() != nil, + Verifier: h.GetVerifier() != nil, + } + + p.signinghandlers[h.GetName()] = h + p.descriptor.SigningHandlers = append(p.descriptor.SigningHandlers, d) + return nil +} + +func (p *plugin) SigningHandlers() []SigningHandler { + return maputils.OrderedValues(p.signinghandlers) +} + +//////////////////////////////////////////////////////////////////////////////// + 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..d20c19600d 100644 --- a/api/ocm/plugin/ppi/utils.go +++ b/api/ocm/plugin/ppi/utils.go @@ -4,37 +4,70 @@ 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" + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/tech/signing" + "ocm.software/ocm/api/utils/cobrautils/flagsets" "ocm.software/ocm/api/utils/runtime" ) type decoder runtime.TypedObjectDecoder[runtime.TypedObject] -type AccessMethodBase struct { +type blobProviderBase struct { decoder nameDescription - version string - format string + format string } -func MustNewAccessMethodBase(name, version string, proto AccessSpec, desc string, format string) AccessMethodBase { +func MustNewBlobProviderBase(name string, proto AccessSpec, desc string, format string) blobProviderBase { decoder, err := runtime.NewDirectDecoder(proto) if err != nil { panic(err) } - return AccessMethodBase{ + return blobProviderBase{ decoder: decoder, nameDescription: nameDescription{ name: name, desc: desc, }, - version: version, - format: format, + format: format, + } +} + +func (b *AccessMethodBase) BlobProviderBase() string { + return b.format +} + +//////////////////////////////////////////////////////////////////////////////// + +type InputTypeBase struct { + blobProviderBase +} + +func MustNewInputTypeBase(name string, proto InputSpec, desc string, format string) InputTypeBase { + return InputTypeBase{MustNewBlobProviderBase(name, proto, desc, format)} +} + +func (b *InputTypeBase) Format() string { + return b.format +} + +//////////////////////////////////////////////////////////////////////////////// + +type AccessMethodBase struct { + blobProviderBase + version string +} + +func MustNewAccessMethodBase(name, version string, proto AccessSpec, desc string, format string) AccessMethodBase { + return AccessMethodBase{ + blobProviderBase: MustNewBlobProviderBase(name, proto, desc, format), + version: version, } } @@ -115,6 +148,192 @@ 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(descriptor.KIND_QUESTION, h.GetQuestion()) + } + for _, e := range t.questions { + if e.GetQuestion() == h.GetQuestion() { + return errors.ErrAlreadyExists(descriptor.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)) +} + +//////////////////////////////////////////////////////////////////////////////// + +type signingHandler struct { + name string + description string + consumer ConsumerProvider + signer signing.Signer + verifier signing.Verifier +} + +func NewSigningHandler(name, desc string, signer signing.Signer) *signingHandler { + return &signingHandler{ + name: name, + description: desc, + signer: signer, + } +} + +func (s *signingHandler) WithVerifier(verifier signing.Verifier) *signingHandler { + s.verifier = verifier + return s +} + +func (s *signingHandler) WithCredentials(p ConsumerProvider) *signingHandler { + s.consumer = p + return s +} + +func (t *signingHandler) GetName() string { + return t.name +} + +func (t *signingHandler) GetDescription() string { + return t.description +} + +func (t *signingHandler) GetSigner() signing.Signer { + return t.signer +} + +func (t *signingHandler) GetVerifier() signing.Verifier { + return t.verifier +} + +func (t *signingHandler) GetConsumerProvider() ConsumerProvider { + return t.consumer +} + +//////////////////////////////////////////////////////////////////////////////// + // Config is a generic structured config stored in a string map. type Config map[string]interface{} @@ -123,7 +342,7 @@ func (c Config) GetValue(name string) (interface{}, bool) { return v, ok } -func (c Config) ConvertFor(list ...options.OptionType) error { +func (c Config) ConvertFor(list ...flagsets.ConfigOptionType) error { for _, o := range list { if v, ok := c[o.GetName()]; ok { t := reflect.TypeOf(o.Create().Value()) diff --git a/api/ocm/plugin/registration/registration.go b/api/ocm/plugin/registration/registration.go index eb8f2da76c..fae76ff997 100644 --- a/api/ocm/plugin/registration/registration.go +++ b/api/ocm/plugin/registration/registration.go @@ -11,6 +11,7 @@ import ( pluginaccess "ocm.software/ocm/api/ocm/extensions/accessmethods/plugin" pluginaction "ocm.software/ocm/api/ocm/extensions/actionhandler/plugin" "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/api/ocm/extensions/attrs/signingattr" pluginupload "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/plugin" "ocm.software/ocm/api/ocm/extensions/download" plugindownload "ocm.software/ocm/api/ocm/extensions/download/handlers/plugin" @@ -20,6 +21,7 @@ import ( "ocm.software/ocm/api/ocm/valuemergehandler" pluginmerge "ocm.software/ocm/api/ocm/valuemergehandler/handlers/plugin" "ocm.software/ocm/api/ocm/valuemergehandler/hpi" + sigplugin "ocm.software/ocm/api/tech/signing/handlers/plugin" "ocm.software/ocm/api/utils/runtime" ) @@ -149,6 +151,17 @@ func RegisterExtensions(ctxp ocm.ContextProvider) error { t := plugin.New(name, s.Description) registry.Register(t) } + + signers := signingattr.Get(ctx).HandlerRegistry() + for _, s := range p.GetDescriptor().SigningHandlers { + if s.Signer { + signers.RegisterSigner(s.Name, sigplugin.NewSigner(p, s.Name)) + } + if s.Verifier { + signers.RegisterVerifier(s.Name, sigplugin.NewVerifier(p, s.Name)) + } + } } + return nil } diff --git a/api/ocm/plugin/utils.go b/api/ocm/plugin/utils.go index f3f15ae8de..05a1195e4f 100644 --- a/api/ocm/plugin/utils.go +++ b/api/ocm/plugin/utils.go @@ -2,6 +2,7 @@ package plugin import ( "encoding/json" + "io" "github.com/opencontainers/go-digest" @@ -10,19 +11,27 @@ import ( "ocm.software/ocm/api/utils/iotools" ) -type AccessDataWriter struct { - plugin Plugin - creds json.RawMessage - accspec json.RawMessage +type BlobAccessWriter struct { + creds json.RawMessage + spec json.RawMessage + getter func(writer io.Writer, creds json.RawMessage, spec json.RawMessage) error } -func NewAccessDataWriter(p Plugin, creds, accspec json.RawMessage) *AccessDataWriter { - return &AccessDataWriter{p, creds, accspec} +func NewAccessDataWriter(p Plugin, creds, spec json.RawMessage) *BlobAccessWriter { + return &BlobAccessWriter{creds, spec, p.Get} } -func (d *AccessDataWriter) WriteTo(w accessio.Writer) (int64, digest.Digest, error) { +func NewInputDataWriter(p Plugin, dir string, creds, spec json.RawMessage) *BlobAccessWriter { + return &BlobAccessWriter{ + creds: creds, + spec: spec, + getter: func(w io.Writer, creds, spec json.RawMessage) error { return p.GetInputBlob(w, dir, creds, spec) }, + } +} + +func (d *BlobAccessWriter) WriteTo(w accessio.Writer) (int64, digest.Digest, error) { dw := iotools.NewDefaultDigestWriter(accessio.NopWriteCloser(w)) - err := d.plugin.Get(dw, d.creds, d.accspec) + err := d.getter(dw, d.creds, d.spec) if err != nil { return blobaccess.BLOB_UNKNOWN_SIZE, blobaccess.BLOB_UNKNOWN_DIGEST, err } 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/tech/signing/handlers/plugin/plugin.go b/api/tech/signing/handlers/plugin/plugin.go new file mode 100644 index 0000000000..a721edfa7f --- /dev/null +++ b/api/tech/signing/handlers/plugin/plugin.go @@ -0,0 +1,52 @@ +package plugin + +import ( + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/credentials/identity/hostpath" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/tech/signing" +) + +type signer struct { + plugin plugin.Plugin + name string +} + +func NewSigner(p plugin.Plugin, name string) signing.Signer { + return &signer{ + plugin: p, + name: name, + } +} + +func NewVerifier(p plugin.Plugin, name string) signing.Verifier { + return &signer{ + plugin: p, + name: name, + } +} + +func (s *signer) Sign(ctx credentials.Context, digest string, sctx signing.SigningContext) (*signing.Signature, error) { + cid, err := s.plugin.GetSigningConsumer(s.name, sctx) + if err != nil { + return nil, err + } + + var creds credentials.DirectCredentials + if len(cid) != 0 { + c, err := credentials.CredentialsForConsumer(ctx, cid, hostpath.IdentityMatcher(cid.Type())) + if err != nil { + return nil, err + } + creds = credentials.DirectCredentials(c.Properties()) + } + return s.plugin.Sign(s.name, digest, creds, sctx) +} + +func (s *signer) Verify(digest string, sig *signing.Signature, sctx signing.SigningContext) error { + return s.plugin.Verify(s.name, digest, sig, sctx) +} + +func (s *signer) Algorithm() string { + return s.name +} diff --git a/api/tech/signing/handlers/plugin/signing_test.go b/api/tech/signing/handlers/plugin/signing_test.go new file mode 100644 index 0000000000..8cd1be67bb --- /dev/null +++ b/api/tech/signing/handlers/plugin/signing_test.go @@ -0,0 +1,190 @@ +//go:build unix + +package plugin_test + +import ( + "crypto" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "ocm.software/ocm/api/credentials" + . "ocm.software/ocm/api/ocm/plugin/testutils" + . "ocm.software/ocm/cmds/ocm/testhelper" + + "ocm.software/ocm/api/tech/signing" + "ocm.software/ocm/api/tech/signing/handlers/plugin" + "ocm.software/ocm/api/tech/signing/handlers/plugin/testdata/plugin/signinghandlers" + "ocm.software/ocm/api/tech/signing/signutils" + "ocm.software/ocm/api/utils/mime" + + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/api/ocm/plugin/plugins" +) + +const PLUGIN = "signing" + +const KEY = "some key" + +const DIGEST = "mydigest" +const DN = "O=acme.org,CN=donald" + +var _ = Describe("Signing Command Test Environment", func() { + Context("plugin execution", func() { + var env *TestEnv + var plugindir TempPluginDir + var registry plugins.Set + + BeforeEach(func() { + env = NewTestEnv(TestData()) + plugindir = Must(ConfigureTestPlugins(env, "testdata/plugins")) + registry = plugincacheattr.Get(env) + }) + + AfterEach(func() { + plugindir.Cleanup() + env.Cleanup() + }) + + It("loads plugin", func() { + // Expect(registration.RegisterExtensions(env)).To(Succeed()) + p := registry.Get(PLUGIN) + Expect(p).NotTo(BeNil()) + Expect(p.Error()).To(Equal("")) + }) + + It("calls signer", func() { + p := registry.Get(PLUGIN) + + sctx := &signing.DefaultSigningContext{ + Hash: crypto.SHA256, + PrivateKey: KEY, + PublicKey: nil, + Issuer: Must(signutils.ParseDN(DN)), + } + + sig := Must(p.Sign(signinghandlers.NAME, DIGEST, nil, sctx)) + Expect(sig).NotTo(BeNil()) + + Expect(sig.Value).To(Equal(DIGEST + ":" + KEY)) + Expect(sig.MediaType).To(Equal(mime.MIME_TEXT)) + Expect(sig.Algorithm).To(Equal(signinghandlers.NAME)) + + dn := Must(signutils.ParseDN(DN)) + Expect(sig.Issuer).To(Equal(signutils.DNAsString(*dn))) + }) + + It("calls verifier", func() { + p := registry.Get(PLUGIN) + + sctx := &signing.DefaultSigningContext{ + Hash: crypto.SHA256, + PrivateKey: nil, + PublicKey: KEY, + Issuer: Must(signutils.ParseDN(DN)), + } + + sig := &signing.Signature{ + Value: DIGEST + ":" + KEY, + MediaType: mime.MIME_TEXT, + Algorithm: signinghandlers.NAME, + Issuer: DN, + } + MustBeSuccessful(p.Verify(signinghandlers.NAME, DIGEST, sig, sctx)) + }) + + Context("extension with creds", func() { + It("calls signer", func() { + p := registry.Get(PLUGIN) + + signer := plugin.NewSigner(p, signinghandlers.NAME) + + env.Context.CredentialsContext().SetCredentialsForConsumer( + credentials.NewConsumerIdentity(signinghandlers.CID_TYPE), + credentials.CredentialsFromList(signinghandlers.SUFFIX_KEY, KEY), + ) + sctx := &signing.DefaultSigningContext{ + Hash: crypto.SHA256, + PrivateKey: signinghandlers.SUFFIX, + Issuer: Must(signutils.ParseDN(DN)), + } + + sig := Must(signer.Sign(env.Context.CredentialsContext(), DIGEST, sctx)) + Expect(sig).NotTo(BeNil()) + + Expect(sig.Value).To(Equal(DIGEST + ":" + signinghandlers.SUFFIX + ":" + KEY)) + Expect(sig.MediaType).To(Equal(mime.MIME_TEXT)) + Expect(sig.Algorithm).To(Equal(signinghandlers.NAME)) + + dn := Must(signutils.ParseDN(DN)) + Expect(sig.Issuer).To(Equal(signutils.DNAsString(*dn))) + }) + + It("calls verifier", func() { + p := registry.Get(PLUGIN) + + verifier := plugin.NewVerifier(p, signinghandlers.NAME) + + sctx := &signing.DefaultSigningContext{ + Hash: crypto.SHA256, + PublicKey: signinghandlers.SUFFIX_KEY + ":" + KEY, + Issuer: Must(signutils.ParseDN(DN)), + } + sig := &signing.Signature{ + Value: DIGEST + ":" + signinghandlers.SUFFIX_KEY + ":" + KEY, + MediaType: mime.MIME_TEXT, + Algorithm: signinghandlers.NAME, + Issuer: DN, + } + + MustBeSuccessful(verifier.Verify(DIGEST, sig, sctx)) + }) + }) + + Context("extension without creds", func() { + It("calls signer", func() { + p := registry.Get(PLUGIN) + + signer := plugin.NewSigner(p, signinghandlers.NAME) + + sctx := &signing.DefaultSigningContext{ + Hash: crypto.SHA256, + PrivateKey: KEY, + Issuer: Must(signutils.ParseDN(DN)), + } + + signer.Sign(nil, DIGEST, sctx) + + sig := Must(signer.Sign(nil, DIGEST, sctx)) + Expect(sig).NotTo(BeNil()) + + Expect(sig.Value).To(Equal(DIGEST + ":" + KEY)) + Expect(sig.MediaType).To(Equal(mime.MIME_TEXT)) + Expect(sig.Algorithm).To(Equal(signinghandlers.NAME)) + + dn := Must(signutils.ParseDN(DN)) + Expect(sig.Issuer).To(Equal(signutils.DNAsString(*dn))) + }) + + It("calls verifier", func() { + p := registry.Get(PLUGIN) + + verifier := plugin.NewVerifier(p, signinghandlers.NAME) + + sctx := &signing.DefaultSigningContext{ + Hash: crypto.SHA256, + PublicKey: KEY, + Issuer: Must(signutils.ParseDN(DN)), + } + sig := &signing.Signature{ + Value: DIGEST + ":" + KEY, + MediaType: mime.MIME_TEXT, + Algorithm: signinghandlers.NAME, + Issuer: DN, + } + + MustBeSuccessful(verifier.Verify(DIGEST, sig, sctx)) + }) + }) + }) +}) diff --git a/api/tech/signing/handlers/plugin/suite_test.go b/api/tech/signing/handlers/plugin/suite_test.go new file mode 100644 index 0000000000..19c18167da --- /dev/null +++ b/api/tech/signing/handlers/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, "Signing Plugin Test Suite") +} diff --git a/api/tech/signing/handlers/plugin/testdata/context b/api/tech/signing/handlers/plugin/testdata/context new file mode 100644 index 0000000000..427df2516e --- /dev/null +++ b/api/tech/signing/handlers/plugin/testdata/context @@ -0,0 +1 @@ +{ "privateKey": "737566666978", "hashAlgorithm": "SHA-256" } diff --git a/api/tech/signing/handlers/plugin/testdata/plugin/app/app.go b/api/tech/signing/handlers/plugin/testdata/plugin/app/app.go new file mode 100644 index 0000000000..627bcf0366 --- /dev/null +++ b/api/tech/signing/handlers/plugin/testdata/plugin/app/app.go @@ -0,0 +1,29 @@ +package app + +import ( + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds" + "ocm.software/ocm/api/tech/signing/handlers/plugin/testdata/plugin/signinghandlers" + "ocm.software/ocm/api/version" +) + +func New() (ppi.Plugin, error) { + p := ppi.NewPlugin("signing", version.Get().String()) + + p.SetShort("fake signing plugin") + p.SetLong("providing fake signing and verification") + + err := p.RegisterSigningHandler(signinghandlers.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/api/tech/signing/handlers/plugin/testdata/plugin/config/config.go b/api/tech/signing/handlers/plugin/testdata/plugin/config/config.go new file mode 100644 index 0000000000..9fcd1691b7 --- /dev/null +++ b/api/tech/signing/handlers/plugin/testdata/plugin/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/api/tech/signing/handlers/plugin/testdata/plugin/main.go b/api/tech/signing/handlers/plugin/testdata/plugin/main.go new file mode 100644 index 0000000000..e1d39492db --- /dev/null +++ b/api/tech/signing/handlers/plugin/testdata/plugin/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "ocm.software/ocm/api/tech/signing/handlers/plugin/testdata/plugin/app" +) + +func main() { + err := app.Run(os.Args[1:]) + if err != nil { + os.Exit(1) + } +} diff --git a/api/tech/signing/handlers/plugin/testdata/plugin/signinghandlers/demo.go b/api/tech/signing/handlers/plugin/testdata/plugin/signinghandlers/demo.go new file mode 100644 index 0000000000..bd0806e24f --- /dev/null +++ b/api/tech/signing/handlers/plugin/testdata/plugin/signinghandlers/demo.go @@ -0,0 +1,112 @@ +package signinghandlers + +import ( + "fmt" + + "github.com/mandelsoft/goutils/errors" + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/tech/signing" + "ocm.software/ocm/api/tech/signing/signutils" + "ocm.software/ocm/api/utils/mime" +) + +const ( + NAME = "demo" +) + +const ( + CID_TYPE = "TEST" + SUFFIX = "suffix" + SUFFIX_KEY = "suffix" +) + +func New() ppi.SigningHandler { + return ppi.NewSigningHandler(NAME, "fake signing", &signer{}).WithVerifier(&verifier{}).WithCredentials(provider) +} + +//////////////////////////////////////////////////////////////////////////////// + +func provider(sctx signing.SigningContext) credentials.ConsumerIdentity { + key, ok := sctx.GetPrivateKey().([]byte) + if !ok { + return nil + } + if string(key) == SUFFIX { + return credentials.NewConsumerIdentity(CID_TYPE, "host", "localhost") + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////// + +type signer struct{} + +var _ signing.Signer = (*signer)(nil) + +func (s signer) Sign(cctx credentials.Context, digest string, sctx signing.SigningContext) (*signing.Signature, error) { + var suffix string + + key, ok := sctx.GetPrivateKey().([]byte) + if !ok { + return nil, fmt.Errorf("invalid private key") + } + + if cctx != nil { + c, err := credentials.CredentialsForConsumer(cctx, provider(sctx)) + if err != nil { + return nil, err + } + if c != nil { + suffix = c.GetProperty(SUFFIX_KEY) + if suffix != "" { + suffix = ":" + suffix + } + } + } + + i := "" + if sctx.GetIssuer() != nil { + i = signutils.DNAsString(*sctx.GetIssuer()) + } + if sctx.GetPrivateKey() == nil { + return nil, fmt.Errorf("private key required") + } + return &signing.Signature{ + Value: digest + ":" + string(key) + suffix, + MediaType: mime.MIME_TEXT, + Algorithm: s.Algorithm(), + Issuer: i, + }, nil +} + +func (s signer) Algorithm() string { + return NAME +} + +//////////////////////////////////////////////////////////////////////////////// + +type verifier struct{} + +var _ signing.Verifier = (*verifier)(nil) + +func (v verifier) Verify(digest string, sig *signing.Signature, sctx signing.SigningContext) error { + if sig.Algorithm != NAME { + return errors.ErrInvalid(signutils.KIND_SIGN_ALGORITHM, sig.Algorithm) + } + if sctx.GetPublicKey() == nil { + return fmt.Errorf("public key required") + } + key, ok := sctx.GetPublicKey().([]byte) + if !ok { + return fmt.Errorf("invalid private key") + } + if sig.Value != digest+":"+string(key) { + return fmt.Errorf("invalid signature %q != %q", sig.Value, digest+":"+string(key)) + } + return nil +} + +func (v verifier) Algorithm() string { + return NAME +} diff --git a/api/tech/signing/handlers/plugin/testdata/plugins/signing b/api/tech/signing/handlers/plugin/testdata/plugins/signing new file mode 100755 index 0000000000..97d52eddbb --- /dev/null +++ b/api/tech/signing/handlers/plugin/testdata/plugins/signing @@ -0,0 +1,2 @@ +#!/bin/bash +go run testdata/plugin/main.go "$@" \ No newline at end of file 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/cobrautils/flagsets/configoptionset.go b/api/utils/cobrautils/flagsets/configoptionset.go index ae846beb2f..d964af3c04 100644 --- a/api/utils/cobrautils/flagsets/configoptionset.go +++ b/api/utils/cobrautils/flagsets/configoptionset.go @@ -11,6 +11,8 @@ import ( type ConfigOptionType interface { GetName() string + GetDescriptionText() string + ValueType() string GetDescription() string Create() Option @@ -293,7 +295,7 @@ func (s *configOptionTypeSet) check(list []ConfigOptionType) error { for _, o := range list { old := s.options[o.GetName()] if old != nil && !old.Equal(o) { - return fmt.Errorf("option type %s doesn't match (%T<->%T)", o.GetName(), o, old) + return fmt.Errorf("option type %q doesn't match ([%T,%s]<->[%T,%s])", o.GetName(), o, o.GetDescription(), old, old.GetDescription()) } } return nil diff --git a/api/utils/cobrautils/flagsets/flagsetscheme/common.go b/api/utils/cobrautils/flagsets/flagsetscheme/common.go new file mode 100644 index 0000000000..c6c9b80b7a --- /dev/null +++ b/api/utils/cobrautils/flagsets/flagsetscheme/common.go @@ -0,0 +1,19 @@ +package flagsetscheme + +import ( + "ocm.software/ocm/api/utils/cobrautils/flagsets" +) + +type TypeInfo interface { + ConfigOptionTypeSetHandler() flagsets.ConfigOptionTypeSetHandler + Description() string + Format() string +} + +type typeInfoImpl struct { + handler flagsets.ConfigOptionTypeSetHandler +} + +func (i *typeInfoImpl) ConfigOptionTypeSetHandler() flagsets.ConfigOptionTypeSetHandler { + return i.handler +} diff --git a/api/utils/cobrautils/flagsets/flagsetscheme/types_options.go b/api/utils/cobrautils/flagsets/flagsetscheme/options.go similarity index 69% rename from api/utils/cobrautils/flagsets/flagsetscheme/types_options.go rename to api/utils/cobrautils/flagsets/flagsetscheme/options.go index f6283b976c..f035fbd841 100644 --- a/api/utils/cobrautils/flagsets/flagsetscheme/types_options.go +++ b/api/utils/cobrautils/flagsets/flagsetscheme/options.go @@ -7,11 +7,27 @@ import ( "ocm.software/ocm/api/utils/runtime/descriptivetype" ) +// OptionTargetWrapper is used as target for option functions, it provides +// setters for fields, which should not be modifiable for a final type object. +type OptionTargetWrapper[T any] struct { + target T + info *typeInfoImpl +} + +func NewOptionTargetWrapper[T any](target T, info *typeInfoImpl) *OptionTargetWrapper[T] { + return &OptionTargetWrapper[T]{ + target: target, + info: info, + } +} + +func (t OptionTargetWrapper[E]) SetConfigHandler(value flagsets.ConfigOptionTypeSetHandler) { + t.info.handler = value +} + //////////////////////////////////////////////////////////////////////////////// -// Access Type Options type OptionTarget interface { - descriptivetype.OptionTarget SetConfigHandler(flagsets.ConfigOptionTypeSetHandler) } diff --git a/api/utils/cobrautils/flagsets/flagsetscheme/scheme.go b/api/utils/cobrautils/flagsets/flagsetscheme/scheme.go index f0ed2ea6c3..4698e3f460 100644 --- a/api/utils/cobrautils/flagsets/flagsetscheme/scheme.go +++ b/api/utils/cobrautils/flagsets/flagsetscheme/scheme.go @@ -8,19 +8,11 @@ import ( "ocm.software/ocm/api/utils/runtime/descriptivetype" ) -// VersionTypedObjectType is the appropriately extended type interface -// based on runtime.VersionTypedObjectType. -type VersionTypedObjectType[T runtime.VersionedTypedObject] interface { - descriptivetype.TypedObjectType[T] - - ConfigOptionTypeSetHandler() flagsets.ConfigOptionTypeSetHandler -} - //////////////////////////////////////////////////////////////////////////////// -// ExtendedTypeScheme is the appropriately extended scheme interface based on +// ExtendedProviderTypeScheme is the appropriately extended scheme interface based on // runtime.TypeScheme supporting an extended config provider interface. -type ExtendedTypeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], P flagsets.ConfigTypeOptionSetConfigProvider] interface { +type ExtendedProviderTypeScheme[T runtime.TypedObject, R TypedObjectType[T], P flagsets.ConfigTypeOptionSetConfigProvider] interface { descriptivetype.TypeScheme[T, R] CreateConfigTypeSetConfigProvider() P @@ -28,11 +20,11 @@ type ExtendedTypeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType Unwrap() TypeScheme[T, R] } -type _TypeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T]] interface { +type _TypeScheme[T runtime.TypedObject, R TypedObjectType[T]] interface { TypeScheme[T, R] } -type typeSchemeWrapper[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], P flagsets.ConfigTypeOptionSetConfigProvider] struct { +type typeSchemeWrapper[T runtime.TypedObject, R TypedObjectType[T], P flagsets.ConfigTypeOptionSetConfigProvider] struct { _TypeScheme[T, R] } @@ -44,24 +36,35 @@ func (s *typeSchemeWrapper[T, R, P]) Unwrap() TypeScheme[T, R] { return s._TypeScheme } +// NewVersionedTypeSchemeWrapper wraps a [VersionedTypeScheme] into a scheme returning a specialized config provider +// by casting the result. The type scheme constructor provides different implementations based on its +// arguments. This method here can be used to provide a type scheme returning the correct type. +func NewVersionedTypeSchemeWrapper[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], P flagsets.ConfigTypeOptionSetConfigProvider](s VersionedTypeScheme[T, R]) ExtendedProviderTypeScheme[T, R, P] { + return &typeSchemeWrapper[T, R, P]{s} +} + // NewTypeSchemeWrapper wraps a [TypeScheme] into a scheme returning a specialized config provider // by casting the result. The type scheme constructor provides different implementations based on its // arguments. This method here can be used to provide a type scheme returning the correct type. -func NewTypeSchemeWrapper[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], P flagsets.ConfigTypeOptionSetConfigProvider](s TypeScheme[T, R]) ExtendedTypeScheme[T, R, P] { +func NewTypeSchemeWrapper[T runtime.TypedObject, R TypedObjectType[T], P flagsets.ConfigTypeOptionSetConfigProvider](s TypeScheme[T, R]) ExtendedProviderTypeScheme[T, R, P] { return &typeSchemeWrapper[T, R, P]{s} } -// TypeScheme is the appropriately extended scheme interface based on +// VersionedTypeScheme is the appropriately extended scheme interface based on // runtime.TypeScheme. -type TypeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T]] interface { - ExtendedTypeScheme[T, R, flagsets.ConfigTypeOptionSetConfigProvider] +type VersionedTypeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T]] interface { + ExtendedProviderTypeScheme[T, R, flagsets.ConfigTypeOptionSetConfigProvider] +} + +type TypeScheme[T runtime.TypedObject, R TypedObjectType[T]] interface { + ExtendedProviderTypeScheme[T, R, flagsets.ConfigTypeOptionSetConfigProvider] } -type _typeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T]] interface { +type _typeScheme[T runtime.TypedObject, R TypedObjectType[T]] interface { descriptivetype.TypeScheme[T, R] } -type typeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], S TypeScheme[T, R]] struct { +type typeScheme[T runtime.TypedObject, R TypedObjectType[T], S TypeScheme[T, R]] struct { cfgname string description string group string @@ -69,16 +72,29 @@ type typeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], S T _typeScheme[T, R] } -func flagExtender[T runtime.VersionedTypedObject, R VersionTypedObjectType[T]](ty R) string { +func flagExtender[T runtime.TypedObject, R TypedObjectType[T]](ty R) string { if h := ty.ConfigOptionTypeSetHandler(); h != nil { return utils.IndentLines(flagsets.FormatConfigOptions(h), " ") } return "" } -// NewTypeScheme provides an TypeScheme implementation based on the interfaces +// NewVersionedTypeScheme provides an VersionedTypeScheme implementation based on the interfaces +// and the default runtime.TypeScheme implementation. +func NewVersionedTypeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], S TypeScheme[T, R]](kindname string, cfgname, typeOption, desc, group string, unknown runtime.Unstructured, acceptUnknown bool, base ...S) VersionedTypeScheme[T, R] { + scheme := descriptivetype.NewVersionedTypeScheme[T, R](kindname, flagExtender[T, R], unknown, acceptUnknown, utils.Optional(base...)) + return &typeScheme[T, R, S]{ + cfgname: cfgname, + description: desc, + group: group, + typeOption: typeOption, + _typeScheme: scheme, + } +} + +// NewTypeScheme provides an VersionedTypeScheme implementation based on the interfaces // and the default runtime.TypeScheme implementation. -func NewTypeScheme[T runtime.VersionedTypedObject, R VersionTypedObjectType[T], S TypeScheme[T, R]](kindname string, cfgname, typeOption, desc, group string, unknown runtime.Unstructured, acceptUnknown bool, base ...S) TypeScheme[T, R] { +func NewTypeScheme[T runtime.TypedObject, R TypedObjectType[T], S TypeScheme[T, R]](kindname string, cfgname, typeOption, desc, group string, unknown runtime.Unstructured, acceptUnknown bool, base ...S) TypeScheme[T, R] { scheme := descriptivetype.NewTypeScheme[T, R](kindname, flagExtender[T, R], unknown, acceptUnknown, utils.Optional(base...)) return &typeScheme[T, R, S]{ cfgname: cfgname, @@ -110,8 +126,8 @@ func (t *typeScheme[T, R, S]) CreateConfigTypeSetConfigProvider() flagsets.Confi } } if t.BaseScheme() != nil { - base := t.BaseScheme() - for _, s := range base.(S).CreateConfigTypeSetConfigProvider().OptionTypeSets() { + base := t.BaseScheme().(S) + for _, s := range base.CreateConfigTypeSetConfigProvider().OptionTypeSets() { if prov.GetTypeSet(s.GetName()) == nil { err := prov.AddTypeSet(s) if err != nil { diff --git a/api/utils/cobrautils/flagsets/flagsetscheme/type.go b/api/utils/cobrautils/flagsets/flagsetscheme/type.go new file mode 100644 index 0000000000..2f82ea96ba --- /dev/null +++ b/api/utils/cobrautils/flagsets/flagsetscheme/type.go @@ -0,0 +1,41 @@ +package flagsetscheme + +import ( + "github.com/mandelsoft/goutils/optionutils" + + "ocm.software/ocm/api/utils/cobrautils/flagsets" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/api/utils/runtime/descriptivetype" +) + +// TypedObjectType is the appropriately extended type interface +// based on runtime.TypedObjectType. +type TypedObjectType[T runtime.TypedObject] interface { + descriptivetype.TypedObjectType[T] + + ConfigOptionTypeSetHandler() flagsets.ConfigOptionTypeSetHandler +} + +//////////////////////////////////////////////////////////////////////////////// + +type TypedObjectTypeObject[E runtime.VersionedTypedObject] struct { + *descriptivetype.TypedObjectTypeObject[E] + typeInfoImpl + validator func(E) error +} + +func NewTypedObjectTypeObject[E runtime.VersionedTypedObject](vt runtime.TypedObjectType[E], opts ...TypeOption) *TypedObjectTypeObject[E] { + target := &TypedObjectTypeObject[E]{ + TypedObjectTypeObject: descriptivetype.NewTypedObjectTypeObject[E](vt, optionutils.FilterMappedOptions[descriptivetype.OptionTarget](opts...)...), + } + t := NewOptionTargetWrapper[*TypedObjectTypeObject[E]](target, &target.typeInfoImpl) + optionutils.ApplyOptions[OptionTarget](t, opts...) + return t.target +} + +func (t *TypedObjectTypeObject[E]) Validate(e E) error { + if t.validator == nil { + return nil + } + return t.validator(e) +} diff --git a/api/utils/cobrautils/flagsets/flagsetscheme/types.go b/api/utils/cobrautils/flagsets/flagsetscheme/types.go deleted file mode 100644 index 1cf41b4820..0000000000 --- a/api/utils/cobrautils/flagsets/flagsetscheme/types.go +++ /dev/null @@ -1,62 +0,0 @@ -package flagsetscheme - -import ( - "github.com/mandelsoft/goutils/optionutils" - - "ocm.software/ocm/api/utils/cobrautils/flagsets" - "ocm.software/ocm/api/utils/runtime" - "ocm.software/ocm/api/utils/runtime/descriptivetype" -) - -type additionalTypeInfo interface { - ConfigOptionTypeSetHandler() flagsets.ConfigOptionTypeSetHandler - Description() string - Format() string -} - -type TypedObjectTypeObject[E runtime.VersionedTypedObject] struct { - *descriptivetype.TypedObjectTypeObject[E] - handler flagsets.ConfigOptionTypeSetHandler - validator func(E) error -} - -var _ additionalTypeInfo = (*TypedObjectTypeObject[runtime.VersionedTypedObject])(nil) - -func NewTypedObjectTypeObject[E runtime.VersionedTypedObject](vt runtime.VersionedTypedObjectType[E], opts ...TypeOption) *TypedObjectTypeObject[E] { - t := NewTypeObjectTarget[E](&TypedObjectTypeObject[E]{ - TypedObjectTypeObject: descriptivetype.NewTypedObjectTypeObject[E](vt), - }) - optionutils.ApplyOptions[OptionTarget](t, opts...) - return t.target -} - -func (t *TypedObjectTypeObject[E]) ConfigOptionTypeSetHandler() flagsets.ConfigOptionTypeSetHandler { - return t.handler -} - -func (t *TypedObjectTypeObject[E]) Validate(e E) error { - if t.validator == nil { - return nil - } - return t.validator(e) -} - -//////////////////////////////////////////////////////////////////////////////// - -// TypeObjectTarget is used as target for option functions, it provides -// setters for fields, which should nor be modifiable for a final type object. -type TypeObjectTarget[E runtime.VersionedTypedObject] struct { - *descriptivetype.TypeObjectTarget[E] - target *TypedObjectTypeObject[E] -} - -func NewTypeObjectTarget[E runtime.VersionedTypedObject](target *TypedObjectTypeObject[E]) *TypeObjectTarget[E] { - return &TypeObjectTarget[E]{ - target: target, - TypeObjectTarget: descriptivetype.NewTypeObjectTarget[E](target.TypedObjectTypeObject), - } -} - -func (t TypeObjectTarget[E]) SetConfigHandler(value flagsets.ConfigOptionTypeSetHandler) { - t.target.handler = value -} diff --git a/api/utils/cobrautils/flagsets/flagsetscheme/versioned.go b/api/utils/cobrautils/flagsets/flagsetscheme/versioned.go new file mode 100644 index 0000000000..0ee9783882 --- /dev/null +++ b/api/utils/cobrautils/flagsets/flagsetscheme/versioned.go @@ -0,0 +1,43 @@ +package flagsetscheme + +import ( + "github.com/mandelsoft/goutils/optionutils" + + "ocm.software/ocm/api/utils/cobrautils/flagsets" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/api/utils/runtime/descriptivetype" +) + +// VersionTypedObjectType is the appropriately extended type interface +// based on runtime.VersionTypedObjectType. +type VersionTypedObjectType[T runtime.VersionedTypedObject] interface { + descriptivetype.VersionedTypedObjectType[T] + + ConfigOptionTypeSetHandler() flagsets.ConfigOptionTypeSetHandler +} + +//////////////////////////////////////////////////////////////////////////////// + +type VersionedTypedObjectTypeObject[E runtime.VersionedTypedObject] struct { + *descriptivetype.VersionedTypedObjectTypeObject[E] + typeInfoImpl + validator func(E) error +} + +var _ TypeInfo = (*VersionedTypedObjectTypeObject[runtime.VersionedTypedObject])(nil) + +func NewVersionedTypedObjectTypeObject[E runtime.VersionedTypedObject](vt runtime.VersionedTypedObjectType[E], opts ...TypeOption) *VersionedTypedObjectTypeObject[E] { + target := &VersionedTypedObjectTypeObject[E]{ + VersionedTypedObjectTypeObject: descriptivetype.NewVersionedTypedObjectTypeObject[E](vt, optionutils.FilterMappedOptions[descriptivetype.OptionTarget](opts...)...), + } + t := NewOptionTargetWrapper[*VersionedTypedObjectTypeObject[E]](target, &target.typeInfoImpl) + optionutils.ApplyOptions[OptionTarget](t, opts...) + return t.target +} + +func (t *VersionedTypedObjectTypeObject[E]) Validate(e E) error { + if t.validator == nil { + return nil + } + return t.validator(e) +} diff --git a/api/ocm/extensions/accessmethods/options/registry.go b/api/utils/cobrautils/flagsets/registry.go similarity index 65% rename from api/ocm/extensions/accessmethods/options/registry.go rename to api/utils/cobrautils/flagsets/registry.go index f45c5dbb16..b5ef53c477 100644 --- a/api/ocm/extensions/accessmethods/options/registry.go +++ b/api/utils/cobrautils/flagsets/registry.go @@ -1,4 +1,4 @@ -package options +package flagsets import ( "sync" @@ -13,10 +13,10 @@ const ( KIND_OPTION = "option" ) -type OptionTypeCreator func(name string, description string) OptionType +type ConfigOptionTypeCreator func(name string, description string) ConfigOptionType type ValueTypeInfo struct { - OptionTypeCreator + ConfigOptionTypeCreator Description string } @@ -24,33 +24,31 @@ func (i ValueTypeInfo) GetDescription() string { return i.Description } -type Registry = *registry - -var DefaultRegistry = New() +type ConfigOptionTypeRegistry = *registry type registry struct { lock sync.RWMutex valueTypes map[string]ValueTypeInfo - optionTypes map[string]OptionType + optionTypes map[string]ConfigOptionType } -func New() Registry { +func NewConfigOptionTypeRegistry() ConfigOptionTypeRegistry { return ®istry{ valueTypes: map[string]ValueTypeInfo{}, - optionTypes: map[string]OptionType{}, + optionTypes: map[string]ConfigOptionType{}, } } -func (r *registry) RegisterOptionType(t OptionType) { +func (r *registry) RegisterOptionType(t ConfigOptionType) { r.lock.Lock() defer r.lock.Unlock() r.optionTypes[t.GetName()] = t } -func (r *registry) RegisterValueType(name string, c OptionTypeCreator, desc string) { +func (r *registry) RegisterValueType(name string, c ConfigOptionTypeCreator, desc string) { r.lock.Lock() defer r.lock.Unlock() - r.valueTypes[name] = ValueTypeInfo{OptionTypeCreator: c, Description: desc} + r.valueTypes[name] = ValueTypeInfo{ConfigOptionTypeCreator: c, Description: desc} } func (r *registry) GetValueType(name string) *ValueTypeInfo { @@ -62,13 +60,13 @@ func (r *registry) GetValueType(name string) *ValueTypeInfo { return nil } -func (r *registry) GetOptionType(name string) OptionType { +func (r *registry) GetOptionType(name string) ConfigOptionType { r.lock.RLock() defer r.lock.RUnlock() return r.optionTypes[name] } -func (r *registry) CreateOptionType(typ, name, desc string) (OptionType, error) { +func (r *registry) CreateOptionType(typ, name, desc string) (ConfigOptionType, error) { r.lock.RLock() defer r.lock.RUnlock() t, ok := r.valueTypes[typ] @@ -76,7 +74,7 @@ func (r *registry) CreateOptionType(typ, name, desc string) (OptionType, error) return nil, errors.ErrUnknown(KIND_OPTIONTYPE, typ) } - n := t.OptionTypeCreator(name, desc) + n := t.ConfigOptionTypeCreator(name, desc) o := r.optionTypes[name] if o != nil { if o.ValueType() != n.ValueType() { diff --git a/api/utils/cobrautils/flagsets/registry_test.go b/api/utils/cobrautils/flagsets/registry_test.go new file mode 100644 index 0000000000..8fe13f02c9 --- /dev/null +++ b/api/utils/cobrautils/flagsets/registry_test.go @@ -0,0 +1,53 @@ +package flagsets_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "ocm.software/ocm/api/utils/cobrautils/flagsets" + + "ocm.software/ocm/api/ocm/extensions/accessmethods/options" +) + +var _ = Describe("registry", func() { + var reg flagsets.ConfigOptionTypeRegistry + + BeforeEach(func() { + reg = flagsets.NewConfigOptionTypeRegistry() + }) + + It("sets and retrieves type", func() { + reg.RegisterValueType("string", flagsets.NewStringOptionType, "string") + + t := reg.GetValueType("string") + Expect(t).NotTo(BeNil()) + + o := Must(reg.CreateOptionType("string", "test", "some test")) + Expect(o.GetName()).To(Equal("test")) + Expect(o.GetDescription()).To(Equal("[*string*] some test")) + }) + + It("sets and retrieves option", func() { + reg.RegisterOptionType(options.HostnameOption) + + t := reg.GetOptionType(options.HostnameOption.GetName()) + Expect(t).NotTo(BeNil()) + }) + + It("creates merges a new type", func() { + reg.RegisterValueType("string", flagsets.NewStringOptionType, "string") + reg.RegisterOptionType(options.HostnameOption) + + o := Must(reg.CreateOptionType("string", options.HostnameOption.GetName(), "some test")) + Expect(o).To(BeIdenticalTo(options.HostnameOption)) + }) + + It("fails creating existing", func() { + reg.RegisterValueType("string", flagsets.NewStringOptionType, "string") + reg.RegisterValueType("int", flagsets.NewIntOptionType, "int") + reg.RegisterOptionType(options.HostnameOption) + + _, err := reg.CreateOptionType("int", options.HostnameOption.GetName(), "some test") + MustFailWithMessage(err, "option \"accessHostname\" already exists") + }) +}) diff --git a/api/utils/cobrautils/flagsets/types.go b/api/utils/cobrautils/flagsets/types.go index b3aaacf097..f205fa9179 100644 --- a/api/utils/cobrautils/flagsets/types.go +++ b/api/utils/cobrautils/flagsets/types.go @@ -1,7 +1,7 @@ package flagsets import ( - "reflect" + "fmt" "github.com/spf13/pflag" @@ -10,9 +10,41 @@ import ( "ocm.software/ocm/api/utils/cobrautils/groups" ) +const ( + TYPE_STRING = "string" + TYPE_STRINGARRAY = "[]string" + TYPE_STRING2STRING = "string=string" + TYPE_INT = "int" + TYPE_BOOL = "bool" + TYPE_YAML = "YAML" + TYPE_STRINGMAPYAML = "map[string]YAML" + TYPE_STRING2YAML = "string=YAML" + TYPE_STRING2STRINGSLICE = "string=string,string" + TYPE_STRINGCOLONSTRINGSLICE = "string:string,string" + TYPE_BYTES = "[]byte" + TYPE_IDENTITYPATH = "[]identity" +) + +func SetBaseTypes(r ConfigOptionTypeRegistry) ConfigOptionTypeRegistry { + r.RegisterValueType(TYPE_STRING, NewStringOptionType, "string value") + r.RegisterValueType(TYPE_STRINGARRAY, NewStringArrayOptionType, "list of string values") + r.RegisterValueType(TYPE_STRING2STRING, NewStringMapOptionType, "string map defined by dedicated assignments") + r.RegisterValueType(TYPE_INT, NewIntOptionType, "integer value") + r.RegisterValueType(TYPE_BOOL, NewBoolOptionType, "boolean flag") + r.RegisterValueType(TYPE_YAML, NewYAMLOptionType, "JSON or YAML document string") + r.RegisterValueType(TYPE_STRINGMAPYAML, NewValueMapYAMLOptionType, "JSON or YAML map") + r.RegisterValueType(TYPE_STRING2YAML, NewValueMapOptionType, "string map with arbitrary values defined by dedicated assignments") + r.RegisterValueType(TYPE_STRING2STRINGSLICE, NewStringSliceMapOptionType, "string map defined by dedicated assignment of comma separated strings") + r.RegisterValueType(TYPE_STRINGCOLONSTRINGSLICE, NewStringSliceMapColonOptionType, "string map defined by dedicated assignment of comma separated strings") + r.RegisterValueType(TYPE_BYTES, NewBytesOptionType, "byte value") + r.RegisterValueType(TYPE_IDENTITYPATH, NewIdentityPathOptionType, "identity path") + return r +} + type TypeOptionBase struct { name string description string + valueType string } func (b *TypeOptionBase) GetName() string { @@ -20,9 +52,23 @@ func (b *TypeOptionBase) GetName() string { } func (b *TypeOptionBase) GetDescription() string { + return fmt.Sprintf("[*%s*] %s", b.ValueType(), b.description) +} + +func (b *TypeOptionBase) GetDescriptionText() string { return b.description } +func (o *TypeOptionBase) ValueType() string { + return o.valueType +} + +func (o *TypeOptionBase) Equal(t ConfigOptionType) bool { + return o.name == t.GetName() && + o.description == t.GetDescriptionText() && + o.valueType == t.ValueType() +} + //////////////////////////////////////////////////////////////////////////////// type OptionBase struct { @@ -44,7 +90,7 @@ func (b *OptionBase) GetName() string { } func (b *OptionBase) Description() string { - return b.otyp.GetDescription() + return b.otyp.GetDescriptionText() } func (b *OptionBase) Changed() bool { @@ -80,14 +126,10 @@ type StringOptionType struct { func NewStringOptionType(name string, description string) ConfigOptionType { return &StringOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_STRING}, } } -func (s *StringOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *StringOptionType) Create() Option { return &StringOption{ OptionBase: NewOptionBase(s), @@ -102,7 +144,7 @@ type StringOption struct { var _ Option = (*StringOption)(nil) func (o *StringOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.StringVarPF(fs, &o.value, o.otyp.GetName(), "", "", o.otyp.GetDescription())) + o.TweakFlag(flag.StringVarPF(fs, &o.value, o.otyp.GetName(), "", "", o.otyp.GetDescriptionText())) } func (o *StringOption) Value() interface{} { @@ -117,14 +159,10 @@ type StringArrayOptionType struct { func NewStringArrayOptionType(name string, description string) ConfigOptionType { return &StringArrayOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_STRINGARRAY}, } } -func (s *StringArrayOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *StringArrayOptionType) Create() Option { return &StringArrayOption{ OptionBase: NewOptionBase(s), @@ -139,87 +177,13 @@ type StringArrayOption struct { var _ Option = (*StringArrayOption)(nil) func (o *StringArrayOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.StringArrayVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.StringArrayVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *StringArrayOption) Value() interface{} { return o.value } -// PathOptionType ////////////////////////////////////////////////////////////////////////////// - -type PathOptionType struct { - TypeOptionBase -} - -func NewPathOptionType(name string, description string) ConfigOptionType { - return &PathOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, - } -} - -func (s *PathOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *PathOptionType) Create() Option { - return &PathOption{ - OptionBase: NewOptionBase(s), - } -} - -type PathOption struct { - OptionBase - value string -} - -var _ Option = (*PathOption)(nil) - -func (o *PathOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.PathVarPF(fs, &o.value, o.otyp.GetName(), "", "", o.otyp.GetDescription())) -} - -func (o *PathOption) Value() interface{} { - return o.value -} - -// PathArrayOptionType ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -type PathArrayOptionType struct { - TypeOptionBase -} - -func NewPathArrayOptionType(name string, description string) ConfigOptionType { - return &PathArrayOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, - } -} - -func (s *PathArrayOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *PathArrayOptionType) Create() Option { - return &PathArrayOption{ - OptionBase: NewOptionBase(s), - } -} - -type PathArrayOption struct { - OptionBase - value []string -} - -var _ Option = (*PathArrayOption)(nil) - -func (o *PathArrayOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.PathArrayVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) -} - -func (o *PathArrayOption) Value() interface{} { - return o.value -} - //////////////////////////////////////////////////////////////////////////////// type BoolOptionType struct { @@ -228,14 +192,10 @@ type BoolOptionType struct { func NewBoolOptionType(name string, description string) ConfigOptionType { return &BoolOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_BOOL}, } } -func (s *BoolOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *BoolOptionType) Create() Option { return &BoolOption{ OptionBase: NewOptionBase(s), @@ -250,7 +210,7 @@ type BoolOption struct { var _ Option = (*BoolOption)(nil) func (o *BoolOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.BoolVarPF(fs, &o.value, o.otyp.GetName(), "", false, o.otyp.GetDescription())) + o.TweakFlag(flag.BoolVarPF(fs, &o.value, o.otyp.GetName(), "", false, o.otyp.GetDescriptionText())) } func (o *BoolOption) Value() interface{} { @@ -265,14 +225,10 @@ type IntOptionType struct { func NewIntOptionType(name string, description string) ConfigOptionType { return &IntOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_INT}, } } -func (s *IntOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *IntOptionType) Create() Option { return &IntOption{ OptionBase: NewOptionBase(s), @@ -287,7 +243,7 @@ type IntOption struct { var _ Option = (*IntOption)(nil) func (o *IntOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.IntVarPF(fs, &o.value, o.otyp.GetName(), "", 0, o.otyp.GetDescription())) + o.TweakFlag(flag.IntVarPF(fs, &o.value, o.otyp.GetName(), "", 0, o.otyp.GetDescriptionText())) } func (o *IntOption) Value() interface{} { @@ -302,14 +258,10 @@ type YAMLOptionType struct { func NewYAMLOptionType(name string, description string) ConfigOptionType { return &YAMLOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_YAML}, } } -func (s *YAMLOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *YAMLOptionType) Create() Option { return &YAMLOption{ OptionBase: NewOptionBase(s), @@ -324,7 +276,7 @@ type YAMLOption struct { var _ Option = (*YAMLOption)(nil) func (o *YAMLOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.YAMLVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.YAMLVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *YAMLOption) Value() interface{} { @@ -339,14 +291,10 @@ type ValueMapYAMLOptionType struct { func NewValueMapYAMLOptionType(name string, description string) ConfigOptionType { return &ValueMapYAMLOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_STRINGMAPYAML}, } } -func (s *ValueMapYAMLOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *ValueMapYAMLOptionType) Create() Option { return &ValueMapYAMLOption{ OptionBase: NewOptionBase(s), @@ -361,7 +309,7 @@ type ValueMapYAMLOption struct { var _ Option = (*ValueMapYAMLOption)(nil) func (o *ValueMapYAMLOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.YAMLVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.YAMLVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *ValueMapYAMLOption) Value() interface{} { @@ -376,14 +324,10 @@ type ValueMapOptionType struct { func NewValueMapOptionType(name string, description string) ConfigOptionType { return &ValueMapOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_STRING2YAML}, } } -func (s *ValueMapOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *ValueMapOptionType) Create() Option { return &ValueMapOption{ OptionBase: NewOptionBase(s), @@ -398,7 +342,7 @@ type ValueMapOption struct { var _ Option = (*ValueMapOption)(nil) func (o *ValueMapOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.StringToValueVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.StringToValueVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *ValueMapOption) Value() interface{} { @@ -413,14 +357,10 @@ type StringMapOptionType struct { func NewStringMapOptionType(name string, description string) ConfigOptionType { return &StringMapOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_STRING2STRING}, } } -func (s *StringMapOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *StringMapOptionType) Create() Option { return &StringMapOption{ OptionBase: NewOptionBase(s), @@ -435,7 +375,7 @@ type StringMapOption struct { var _ Option = (*StringMapOption)(nil) func (o *StringMapOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.StringToStringVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.StringToStringVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *StringMapOption) Value() interface{} { @@ -450,14 +390,10 @@ type BytesOptionType struct { func NewBytesOptionType(name string, description string) ConfigOptionType { return &BytesOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_BYTES}, } } -func (s *BytesOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *BytesOptionType) Create() Option { return &BytesOption{ OptionBase: NewOptionBase(s), @@ -472,7 +408,7 @@ type BytesOption struct { var _ Option = (*BytesOption)(nil) func (o *BytesOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.BytesBase64VarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.BytesBase64VarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *BytesOption) Value() interface{} { @@ -487,14 +423,10 @@ type StringSliceMapOptionType struct { func NewStringSliceMapOptionType(name string, description string) ConfigOptionType { return &StringSliceMapOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_STRING2STRINGSLICE}, } } -func (s *StringSliceMapOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *StringSliceMapOptionType) Create() Option { return &StringSliceMapOption{ OptionBase: NewOptionBase(s), @@ -509,7 +441,7 @@ type StringSliceMapOption struct { var _ Option = (*StringSliceMapOption)(nil) func (o *StringSliceMapOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.StringToStringSliceVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.StringToStringSliceVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *StringSliceMapOption) Value() interface{} { @@ -524,14 +456,10 @@ type StringSliceMapColonOptionType struct { func NewStringSliceMapColonOptionType(name string, description string) ConfigOptionType { return &StringSliceMapColonOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_STRINGCOLONSTRINGSLICE}, } } -func (s *StringSliceMapColonOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *StringSliceMapColonOptionType) Create() Option { return &StringSliceMapColonOption{ OptionBase: NewOptionBase(s), @@ -546,7 +474,7 @@ type StringSliceMapColonOption struct { var _ Option = (*StringSliceMapColonOption)(nil) func (o *StringSliceMapColonOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.StringColonStringSliceVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.StringColonStringSliceVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *StringSliceMapColonOption) Value() interface{} { @@ -561,14 +489,10 @@ type IdentityPathOptionType struct { func NewIdentityPathOptionType(name string, description string) ConfigOptionType { return &IdentityPathOptionType{ - TypeOptionBase: TypeOptionBase{name, description}, + TypeOptionBase: TypeOptionBase{name, description, TYPE_IDENTITYPATH}, } } -func (s *IdentityPathOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - func (s *IdentityPathOptionType) Create() Option { return &IdentityPathOption{ OptionBase: NewOptionBase(s), @@ -583,7 +507,7 @@ type IdentityPathOption struct { var _ Option = (*IdentityPathOption)(nil) func (o *IdentityPathOption) AddFlags(fs *pflag.FlagSet) { - o.TweakFlag(flag.IdentityPathVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescription())) + o.TweakFlag(flag.IdentityPathVarPF(fs, &o.value, o.otyp.GetName(), "", nil, o.otyp.GetDescriptionText())) } func (o *IdentityPathOption) Value() interface{} { diff --git a/api/utils/runtime/descriptivetype/common.go b/api/utils/runtime/descriptivetype/common.go new file mode 100644 index 0000000000..b42a95a3c0 --- /dev/null +++ b/api/utils/runtime/descriptivetype/common.go @@ -0,0 +1,27 @@ +package descriptivetype + +// TypeInfo is the interface extension for descriptive types. +type TypeInfo interface { + Description() string + Format() string +} + +type typeInfoImpl struct { + description string + format string +} + +var _ TypeInfo = (*typeInfoImpl)(nil) + +func (i *typeInfoImpl) Description() string { + return i.description +} + +func (i *typeInfoImpl) Format() string { + return i.format +} + +// DescriptionExtender provides an additional description for a type object +// which is appended to the format description in the scheme description +// for the type in question. +type DescriptionExtender[T any] func(t T) string diff --git a/api/utils/runtime/descriptivetype/options.go b/api/utils/runtime/descriptivetype/options.go index 730b05a4bc..6dab03c221 100644 --- a/api/utils/runtime/descriptivetype/options.go +++ b/api/utils/runtime/descriptivetype/options.go @@ -2,28 +2,31 @@ package descriptivetype import ( "github.com/mandelsoft/goutils/optionutils" - - "ocm.software/ocm/api/utils/runtime" ) //////////////////////////////////////////////////////////////////////////////// -// TypeObjectTarget is used as target for option functions, it provides +// OptionTargetWrapper is used as target for option functions, it provides // setters for fields, which should not be modifiable for a final type object. -type TypeObjectTarget[E runtime.VersionedTypedObject] struct { - target *TypedObjectTypeObject[E] +type OptionTargetWrapper[T any] struct { + target T + info *typeInfoImpl +} + +func NewOptionTargetWrapper[T any](target T, info *typeInfoImpl) *OptionTargetWrapper[T] { + return &OptionTargetWrapper[T]{target, info} } -func NewTypeObjectTarget[E runtime.VersionedTypedObject](target *TypedObjectTypeObject[E]) *TypeObjectTarget[E] { - return &TypeObjectTarget[E]{target} +func (t *OptionTargetWrapper[T]) SetDescription(value string) { + t.info.description = value } -func (t *TypeObjectTarget[E]) SetDescription(value string) { - t.target.description = value +func (t *OptionTargetWrapper[T]) SetFormat(value string) { + t.info.format = value } -func (t *TypeObjectTarget[E]) SetFormat(value string) { - t.target.format = value +func (t *OptionTargetWrapper[T]) Target() T { + return t.target } //////////////////////////////////////////////////////////////////////////////// diff --git a/api/utils/runtime/descriptivetype/scheme.go b/api/utils/runtime/descriptivetype/scheme.go new file mode 100644 index 0000000000..193fa97975 --- /dev/null +++ b/api/utils/runtime/descriptivetype/scheme.go @@ -0,0 +1,132 @@ +package descriptivetype + +import ( + "fmt" + "strings" + + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/runtime" +) + +// TypeScheme is the appropriately extended scheme interface based on +// runtime.TypeScheme. Based on the additional type info a complete +// scheme description can be created calling the Describe method. +type TypeScheme[T runtime.TypedObject, R TypedObjectType[T]] interface { + runtime.TypeScheme[T, R] + + Describe() string +} + +type _typeScheme[T runtime.TypedObject, R runtime.TypedObjectType[T]] interface { + runtime.TypeScheme[T, R] // for goland to be able to accept extender argument type +} + +type typeScheme[T runtime.TypedObject, R TypedObjectType[T], S runtime.TypeScheme[T, R]] struct { + name string + extender DescriptionExtender[R] + versioned bool + _typeScheme[T, R] +} + +func MustNewDefaultTypeScheme[T runtime.TypedObject, R TypedObjectType[T], S TypeScheme[T, R]](name string, extender DescriptionExtender[R], unknown runtime.Unstructured, acceptUnknown bool, defaultdecoder runtime.TypedObjectDecoder[T], base ...TypeScheme[T, R]) TypeScheme[T, R] { + scheme := runtime.MustNewDefaultTypeScheme[T, R](unknown, acceptUnknown, defaultdecoder, utils.Optional(base...)) + return &typeScheme[T, R, S]{ + name: name, + extender: extender, + _typeScheme: scheme, + } +} + +// NewTypeScheme provides an TypeScheme implementation based on the interfaces +// and the default runtime.TypeScheme implementation. +func NewTypeScheme[T runtime.TypedObject, R TypedObjectType[T], S TypeScheme[T, R]](name string, extender DescriptionExtender[R], unknown runtime.Unstructured, acceptUnknown bool, base ...S) TypeScheme[T, R] { + scheme := runtime.MustNewDefaultTypeScheme[T, R](unknown, acceptUnknown, nil, utils.Optional(base...)) + return &typeScheme[T, R, S]{ + name: name, + extender: extender, + _typeScheme: scheme, + } +} + +//////////////////////////////////////////////////////////////////////////////// + +func (t *typeScheme[T, R, S]) KnownTypes() runtime.KnownTypes[T, R] { + return t._typeScheme.KnownTypes() // Goland +} + +func (t *typeScheme[T, R, S]) Describe() string { + s := "" + type method struct { + desc string + versions map[string]string + more string + } + + descs := map[string]*method{} + + // gather info for kinds and versions + for _, n := range t.KnownTypeNames() { + var kind, vers string + if t.versioned { + kind, vers = runtime.KindVersion(n) + } else { + kind = n + } + + info := descs[kind] + if info == nil { + info = &method{versions: map[string]string{}} + descs[kind] = info + } + + if vers == "" { + vers = "v1" + } + if _, ok := info.versions[vers]; !ok { + info.versions[vers] = "" + } + + ty := t.GetType(n) + + if t.extender != nil { + more := t.extender(ty) + if more != "" { + info.more = more + } + } + desc := ty.Description() + if desc != "" { + info.desc = desc + } + + desc = ty.Format() + if desc != "" { + info.versions[vers] = desc + } + } + + for _, tn := range utils.StringMapKeys(descs) { + info := descs[tn] + desc := strings.Trim(info.desc, "\n") + if desc != "" { + s = fmt.Sprintf("%s\n- %s %s\n\n%s\n\n", s, t.name, tn, utils.IndentLines(desc, " ")) + + format := "" + for _, f := range utils.StringMapKeys(info.versions) { + desc = strings.Trim(info.versions[f], "\n") + if desc != "" { + if t.versioned { + format = fmt.Sprintf("%s\n- Version %s\n\n%s\n", format, f, utils.IndentLines(desc, " ")) + } else { + s += utils.IndentLines(desc, " ") + } + } + } + if format != "" { + s += fmt.Sprintf(" The following versions are supported:\n%s\n", strings.Trim(utils.IndentLines(format, " "), "\n")) + } + } + s += info.more + } + return s +} diff --git a/api/utils/runtime/descriptivetype/type.go b/api/utils/runtime/descriptivetype/type.go index 6a5ad78eef..c58508331d 100644 --- a/api/utils/runtime/descriptivetype/type.go +++ b/api/utils/runtime/descriptivetype/type.go @@ -1,175 +1,34 @@ package descriptivetype import ( - "fmt" - "strings" - - "ocm.software/ocm/api/utils" "ocm.software/ocm/api/utils/runtime" ) -// DescriptionExtender provides an additional description for a type object -// which is appended to the format description in the scheme description -// for the type in question. -type DescriptionExtender[T any] func(t T) string - // TypedObjectType is the appropriately extended type interface // based on runtime.VersionTypedObjectType providing support for a functional and // format description. -type TypedObjectType[T runtime.VersionedTypedObject] interface { - runtime.VersionedTypedObjectType[T] - - Description() string - Format() string +type TypedObjectType[T runtime.TypedObject] interface { + runtime.TypedObjectType[T] + TypeInfo } //////////////////////////////////////////////////////////////////////////////// -// TypeScheme is the appropriately extended scheme interface based on -// runtime.TypeScheme. Based on the additional type info a complete -// scheme description can be created calling the Describe method. -type TypeScheme[T runtime.VersionedTypedObject, R TypedObjectType[T]] interface { - runtime.TypeScheme[T, R] - - Describe() string -} - -type _typeScheme[T runtime.VersionedTypedObject, R TypedObjectType[T]] interface { - runtime.TypeScheme[T, R] // for goland to be able to accept extender argument type -} - -type typeScheme[T runtime.VersionedTypedObject, R TypedObjectType[T], S TypeScheme[T, R]] struct { - name string - extender DescriptionExtender[R] - _typeScheme[T, R] -} - -func MustNewDefaultTypeScheme[T runtime.VersionedTypedObject, R TypedObjectType[T], S TypeScheme[T, R]](name string, extender DescriptionExtender[R], unknown runtime.Unstructured, acceptUnknown bool, defaultdecoder runtime.TypedObjectDecoder[T], base ...TypeScheme[T, R]) TypeScheme[T, R] { - scheme := runtime.MustNewDefaultTypeScheme[T, R](unknown, acceptUnknown, defaultdecoder, utils.Optional(base...)) - return &typeScheme[T, R, S]{ - name: name, - extender: extender, - _typeScheme: scheme, - } +type TypedObjectTypeObject[T runtime.TypedObject] struct { + runtime.TypedObjectType[T] + typeInfoImpl + validator func(T) error } -// NewTypeScheme provides an TypeScheme implementation based on the interfaces -// and the default runtime.TypeScheme implementation. -func NewTypeScheme[T runtime.VersionedTypedObject, R TypedObjectType[T], S TypeScheme[T, R]](name string, extender DescriptionExtender[R], unknown runtime.Unstructured, acceptUnknown bool, base ...S) TypeScheme[T, R] { - scheme := runtime.MustNewDefaultTypeScheme[T, R](unknown, acceptUnknown, nil, utils.Optional(base...)) - return &typeScheme[T, R, S]{ - name: name, - extender: extender, - _typeScheme: scheme, - } -} - -func (t *typeScheme[T, R, S]) KnownTypes() runtime.KnownTypes[T, R] { - return t._typeScheme.KnownTypes() // Goland -} - -//////////////////////////////////////////////////////////////////////////////// - -func (t *typeScheme[T, R, S]) Describe() string { - s := "" - type method struct { - desc string - versions map[string]string - more string - } - - descs := map[string]*method{} - - // gather info for kinds and versions - for _, n := range t.KnownTypeNames() { - kind, vers := runtime.KindVersion(n) - - info := descs[kind] - if info == nil { - info = &method{versions: map[string]string{}} - descs[kind] = info - } - - if vers == "" { - vers = "v1" - } - if _, ok := info.versions[vers]; !ok { - info.versions[vers] = "" - } - - ty := t.GetType(n) - - if t.extender != nil { - more := t.extender(ty) - if more != "" { - info.more = more - } - } - desc := ty.Description() - if desc != "" { - info.desc = desc - } - - desc = ty.Format() - if desc != "" { - info.versions[vers] = desc - } +func NewTypedObjectTypeObject[E runtime.VersionedTypedObject](vt runtime.TypedObjectType[E], opts ...Option) *TypedObjectTypeObject[E] { + target := &TypedObjectTypeObject[E]{ + TypedObjectType: vt, } - - for _, tn := range utils.StringMapKeys(descs) { - info := descs[tn] - desc := strings.Trim(info.desc, "\n") - if desc != "" { - s = fmt.Sprintf("%s\n- %s %s\n\n%s\n\n", s, t.name, tn, utils.IndentLines(desc, " ")) - - format := "" - for _, f := range utils.StringMapKeys(info.versions) { - desc = strings.Trim(info.versions[f], "\n") - if desc != "" { - format = fmt.Sprintf("%s\n- Version %s\n\n%s\n", format, f, utils.IndentLines(desc, " ")) - } - } - if format != "" { - s += fmt.Sprintf(" The following versions are supported:\n%s\n", strings.Trim(utils.IndentLines(format, " "), "\n")) - } - } - s += info.more - } - return s -} - -//////////////////////////////////////////////////////////////////////////////// - -type descriptiveTypeInfo interface { - Description() string - Format() string -} - -type TypedObjectTypeObject[T runtime.VersionedTypedObject] struct { - runtime.VersionedTypedObjectType[T] - description string - format string - validator func(T) error -} - -var _ descriptiveTypeInfo = (*TypedObjectTypeObject[runtime.VersionedTypedObject])(nil) - -func NewTypedObjectTypeObject[E runtime.VersionedTypedObject](vt runtime.VersionedTypedObjectType[E], opts ...Option) *TypedObjectTypeObject[E] { - t := NewTypeObjectTarget[E](&TypedObjectTypeObject[E]{ - VersionedTypedObjectType: vt, - }) + t := NewOptionTargetWrapper(target, &target.typeInfoImpl) for _, o := range opts { o.ApplyTo(t) } - return t.target -} - -func (t *TypedObjectTypeObject[T]) Description() string { - return t.description -} - -func (t *TypedObjectTypeObject[T]) Format() string { - return t.format + return target } func (t *TypedObjectTypeObject[T]) Validate(e T) error { diff --git a/api/utils/runtime/descriptivetype/versionedtype.go b/api/utils/runtime/descriptivetype/versionedtype.go new file mode 100644 index 0000000000..9c75b8877b --- /dev/null +++ b/api/utils/runtime/descriptivetype/versionedtype.go @@ -0,0 +1,71 @@ +package descriptivetype + +import ( + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/runtime" +) + +// VersionedTypedObjectType is the appropriately extended type interface +// based on runtime.VersionTypedObjectType providing support for a functional and +// format description. +type VersionedTypedObjectType[T runtime.VersionedTypedObject] interface { + runtime.VersionedTypedObjectType[T] + TypeInfo +} + +//////////////////////////////////////////////////////////////////////////////// + +// VersionedTypeScheme is the appropriately extended scheme interface based on +// runtime.TypeScheme. Based on the additional type info a complete +// scheme description can be created calling the Describe method. +type VersionedTypeScheme[T runtime.VersionedTypedObject, R VersionedTypedObjectType[T]] interface { + TypeScheme[T, R] +} + +func MustNewDefaultVersionedTypeScheme[T runtime.VersionedTypedObject, R VersionedTypedObjectType[T], S VersionedTypeScheme[T, R]](name string, extender DescriptionExtender[R], unknown runtime.Unstructured, acceptUnknown bool, defaultdecoder runtime.TypedObjectDecoder[T], base ...VersionedTypeScheme[T, R]) VersionedTypeScheme[T, R] { + scheme := runtime.MustNewDefaultTypeScheme[T, R](unknown, acceptUnknown, defaultdecoder, utils.Optional(base...)) + return &typeScheme[T, R, S]{ + name: name, + extender: extender, + _typeScheme: scheme, + versioned: true, + } +} + +// NewVersionedTypeScheme provides an TypeScheme implementation based on the interfaces +// and the default runtime.TypeScheme implementation. +func NewVersionedTypeScheme[T runtime.VersionedTypedObject, R VersionedTypedObjectType[T], S VersionedTypeScheme[T, R]](name string, extender DescriptionExtender[R], unknown runtime.Unstructured, acceptUnknown bool, base ...S) VersionedTypeScheme[T, R] { + scheme := runtime.MustNewDefaultTypeScheme[T, R](unknown, acceptUnknown, nil, utils.Optional(base...)) + return &typeScheme[T, R, S]{ + name: name, + extender: extender, + _typeScheme: scheme, + versioned: true, + } +} + +//////////////////////////////////////////////////////////////////////////////// + +type VersionedTypedObjectTypeObject[T runtime.VersionedTypedObject] struct { + runtime.VersionedTypedObjectType[T] + typeInfoImpl + validator func(T) error +} + +func NewVersionedTypedObjectTypeObject[E runtime.VersionedTypedObject](vt runtime.VersionedTypedObjectType[E], opts ...Option) *VersionedTypedObjectTypeObject[E] { + target := &VersionedTypedObjectTypeObject[E]{ + VersionedTypedObjectType: vt, + } + t := NewOptionTargetWrapper(target, &target.typeInfoImpl) + for _, o := range opts { + o.ApplyTo(t) + } + return target +} + +func (t *VersionedTypedObjectTypeObject[T]) Validate(e T) error { + if t.validator == nil { + return nil + } + return t.validator(e) +} 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/runtime/scheme.go b/api/utils/runtime/scheme.go index 61ebf4d682..52bdd5092a 100644 --- a/api/utils/runtime/scheme.go +++ b/api/utils/runtime/scheme.go @@ -215,6 +215,33 @@ func NewDefaultScheme[T TypedObject, R TypedObjectDecoder[T]](protoUnstr Unstruc }, nil } +type Copyable[T any] interface { + Copy() T +} + +// CopyScheme copies a Copyable scheme. +// This is not a method on Scheme, because it would be propagated by embedding +// a scheme to get the traditional methods, but would return an internal +// implementation detail. +func CopyScheme[T TypedObject, R TypedObjectDecoder[T]](s Scheme[T, R]) Scheme[T, R] { + if c, ok := s.(Copyable[Scheme[T, R]]); ok { + return c.Copy() + } + return nil +} + +func (d *defaultScheme[T, R]) Copy() Scheme[T, R] { + scheme := &defaultScheme[T, R]{ + instance: d.instance, + unstructured: d.unstructured, + defaultdecoder: d.defaultdecoder, + acceptUnknown: d.acceptUnknown, + types: KnownTypes[T, R]{}, + } + scheme.AddKnownTypes(d) + return scheme +} + func (d *defaultScheme[T, R]) BaseScheme() Scheme[T, R] { return d.base } 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..079e74a3bc 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" @@ -36,7 +37,7 @@ type AccessMethod struct { ppi.AccessMethodBase } -var PathOption = options.NewStringOptionType("accessPath", "path in temp repository") +var PathOption = flagsets.NewStringOptionType("accessPath", "path in temp repository") var _ ppi.AccessMethod = (*AccessMethod)(nil) @@ -46,8 +47,8 @@ func New() ppi.AccessMethod { } } -func (a *AccessMethod) Options() []options.OptionType { - return []options.OptionType{ +func (a *AccessMethod) Options() []flagsets.ConfigOptionType { + return []flagsets.ConfigOptionType{ options.MediatypeOption, PathOption, } @@ -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/demoplugin/valuesets/check.go b/cmds/demoplugin/valuesets/check.go index 0e75372aff..729428ca6c 100644 --- a/cmds/demoplugin/valuesets/check.go +++ b/cmds/demoplugin/valuesets/check.go @@ -7,7 +7,6 @@ import ( "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/goutils/set" - "ocm.software/ocm/api/ocm/extensions/accessmethods/options" "ocm.software/ocm/api/ocm/plugin/descriptor" "ocm.software/ocm/api/ocm/plugin/ppi" "ocm.software/ocm/api/utils" @@ -36,8 +35,8 @@ const ( var status = set.New[string](STATUS_PASSED, STATUS_FAILED, STATUS_SKIPPED) var ( - StatusOption = options.NewStringMapOptionType("checkStatus", out.Sprintf("status value for check (%s)", strings.Join(utils.StringMapKeys(status), ", "))) - MessageOption = options.NewStringMapOptionType("checkMessage", "message for check") + StatusOption = flagsets.NewStringMapOptionType("checkStatus", out.Sprintf("status value for check (%s)", strings.Join(utils.StringMapKeys(status), ", "))) + MessageOption = flagsets.NewStringMapOptionType("checkMessage", "message for check") ) type ValueSet struct { @@ -56,8 +55,8 @@ The status entry has the following format: } } -func (v ValueSet) Options() []options.OptionType { - return []options.OptionType{ +func (v ValueSet) Options() []flagsets.ConfigOptionType { + return []flagsets.ConfigOptionType{ StatusOption, MessageOption, } diff --git a/cmds/ocm/app/app.go b/cmds/ocm/app/app.go index d0266a7f4d..f9e6beab60 100644 --- a/cmds/ocm/app/app.go +++ b/cmds/ocm/app/app.go @@ -30,6 +30,7 @@ import ( creds "ocm.software/ocm/cmds/ocm/commands/misccmds/credentials" "ocm.software/ocm/cmds/ocm/commands/ocicmds" "ocm.software/ocm/cmds/ocm/commands/ocmcmds" + inputplugins "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/componentarchive" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/components" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" @@ -393,6 +394,7 @@ func (o *CLIOptions) Complete() error { if err != nil { return err } + inputplugins.RegisterPlugins(o.Context) return o.Context.ConfigContext().Validate() } diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go index c5fdf9a3b3..946a028b40 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go @@ -117,11 +117,13 @@ func (h *ResourceSpecHandler) Add(ctx clictx.Context, ictx inputs.Context, elem cd := cv.GetDescriptor() - opts := h.srchandler.AsOptionSet()[0].(*addhdlrs.Options) - if !opts.Replace { - cd.Resources = nil - cd.Sources = nil - cd.References = nil + if len(h.srchandler.AsOptionSet()) > 0 { + opts := h.srchandler.AsOptionSet()[0].(*addhdlrs.Options) + if !opts.Replace { + cd.Resources = nil + cd.Sources = nil + cd.References = nil + } } schema := h.schema.Schema diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/cpi/helper.go b/cmds/ocm/commands/ocmcmds/common/inputs/cpi/helper.go index 2646ae99a7..0bf2e21f60 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/cpi/helper.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/cpi/helper.go @@ -109,12 +109,12 @@ func (s *ProcessSpec) ProcessBlob(ctx inputs.Context, acc blobaccess.DataAccess, } func AddProcessSpecOptionTypes(set flagsets.ConfigOptionTypeSetHandler) { - set.AddOptionType(options.MediaTypeOption) + set.AddOptionType(options.MediatypeOption) set.AddOptionType(options.CompressOption) } func AddProcessSpecConfig(opts flagsets.ConfigOptions, config flagsets.Config) error { - flagsets.AddFieldByOptionP(opts, options.MediaTypeOption, config, "mediaType") + flagsets.AddFieldByOptionP(opts, options.MediatypeOption, config, "mediaType") flagsets.AddFieldByOptionP(opts, options.CompressOption, config, "compress") return nil } diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/extend_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/extend_test.go new file mode 100644 index 0000000000..eeef9db3d5 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/extend_test.go @@ -0,0 +1,160 @@ +package inputs_test + +import ( + "fmt" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/api/datacontext" + + "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/util/validation/field" + + "ocm.software/ocm/api/utils/blobaccess/blobaccess" + "ocm.software/ocm/api/utils/cobrautils/flagsets" + "ocm.software/ocm/api/utils/mime" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/cpi" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/options" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact" +) + +var _ = Describe("Input Type Extension Test Environment", func() { + var ( + scheme inputs.InputTypeScheme + itype = inputs.NewInputType(TYPE, &Spec{}, "", ConfigHandler()) + flags *pflag.FlagSet + opts flagsets.ConfigOptions + ) + + Context("registry", func() { + BeforeEach(func() { + scheme = inputs.NewInputTypeScheme(nil, inputs.DefaultInputTypeScheme) + scheme.Register(itype) + flags = &pflag.FlagSet{} + opts = scheme.CreateConfigTypeSetConfigProvider().CreateOptions() + opts.AddFlags(flags) + }) + + It("is not in base", func() { + scheme = inputs.DefaultInputTypeScheme + Expect(scheme.GetInputType(TYPE)).To(BeNil()) + }) + + It("derives base input type", func() { + prov := scheme.CreateConfigTypeSetConfigProvider() + MustBeSuccessful(flagsets.ParseOptionsFor(flags, + flagsets.OptionSpec(prov.GetTypeOptionType(), ociartifact.TYPE), + flagsets.OptionSpec(options.PathOption, "ghcr.io/open-component-model/image:v1.0"), + flagsets.OptionSpec(options.PlatformsOption, "linux/amd64"), + flagsets.OptionSpec(options.PlatformsOption, "/arm64"), + )) + cfg := Must(prov.GetConfigFor(opts)) + fmt.Printf("selected input options: %+v\n", cfg) + + spec := Must(scheme.GetInputSpecFor(cfg)) + Expect(spec).To(Equal(ociartifact.New("ghcr.io/open-component-model/image:v1.0", "linux/amd64", "/arm64"))) + }) + + It("uses extended input type", func() { + prov := scheme.CreateConfigTypeSetConfigProvider() + MustBeSuccessful(flagsets.ParseOptionsFor(flags, + flagsets.OptionSpec(prov.GetTypeOptionType(), TYPE), + flagsets.OptionSpec(options.PathOption, "ghcr.io/open-component-model/image:v1.0"), + )) + cfg := Must(prov.GetConfigFor(opts)) + fmt.Printf("selected input options: %+v\n", cfg) + + spec := Must(scheme.GetInputSpecFor(cfg)) + Expect(spec).To(Equal(New("ghcr.io/open-component-model/image:v1.0"))) + }) + }) + + Context("cli context", func() { + var ctx clictx.Context + + BeforeEach(func() { + ctx = clictx.New(datacontext.MODE_EXTENDED) + scheme = inputs.For(ctx) + scheme.Register(itype) + flags = &pflag.FlagSet{} + opts = scheme.CreateConfigTypeSetConfigProvider().CreateOptions() + opts.AddFlags(flags) + }) + + It("is not in base", func() { + scheme = inputs.For(clictx.DefaultContext()) + Expect(scheme.GetInputType(TYPE)).To(BeNil()) + }) + + It("derives base input type", func() { + prov := scheme.CreateConfigTypeSetConfigProvider() + MustBeSuccessful(flagsets.ParseOptionsFor(flags, + flagsets.OptionSpec(prov.GetTypeOptionType(), ociartifact.TYPE), + flagsets.OptionSpec(options.PathOption, "ghcr.io/open-component-model/image:v1.0"), + flagsets.OptionSpec(options.PlatformsOption, "linux/amd64"), + flagsets.OptionSpec(options.PlatformsOption, "/arm64"), + )) + cfg := Must(prov.GetConfigFor(opts)) + fmt.Printf("selected input options: %+v\n", cfg) + + spec := Must(scheme.GetInputSpecFor(cfg)) + Expect(spec).To(Equal(ociartifact.New("ghcr.io/open-component-model/image:v1.0", "linux/amd64", "/arm64"))) + }) + + It("uses extended input type", func() { + prov := scheme.CreateConfigTypeSetConfigProvider() + MustBeSuccessful(flagsets.ParseOptionsFor(flags, + flagsets.OptionSpec(prov.GetTypeOptionType(), TYPE), + flagsets.OptionSpec(options.PathOption, "ghcr.io/open-component-model/image:v1.0"), + )) + cfg := Must(prov.GetConfigFor(opts)) + fmt.Printf("selected input options: %+v\n", cfg) + + spec := Must(scheme.GetInputSpecFor(cfg)) + Expect(spec).To(Equal(New("ghcr.io/open-component-model/image:v1.0"))) + }) + }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// test input + +const TYPE = "testinput" + +type Spec struct { + // PathSpec holds the repository path and tag of the image in the docker daemon + cpi.PathSpec +} + +var _ inputs.InputSpec = (*Spec)(nil) + +func New(pathtag string) *Spec { + return &Spec{ + PathSpec: cpi.NewPathSpec(TYPE, pathtag), + } +} + +func (s *Spec) Validate(fldPath *field.Path, ctx inputs.Context, inputFilePath string) field.ErrorList { + allErrs := s.PathSpec.Validate(fldPath, ctx, inputFilePath) + return allErrs +} + +func (s *Spec) GetBlob(ctx inputs.Context, info inputs.InputResourceInfo) (blobaccess.BlobAccess, string, error) { + blob := blobaccess.ForString(mime.MIME_TEXT, s.Path) + return blob, "", nil +} + +func ConfigHandler() flagsets.ConfigOptionTypeSetHandler { + return cpi.NewMediaFileSpecOptionType(TYPE, AddConfig, + options.PathOption, options.HintOption, options.PlatformsOption) +} + +func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error { + if err := cpi.AddPathSpecConfig(opts, config); err != nil { + return err + } + return nil +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go b/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go index 39a233d97f..ec60849a2c 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go @@ -15,6 +15,7 @@ import ( "ocm.software/ocm/api/utils" "ocm.software/ocm/api/utils/blobaccess" "ocm.software/ocm/api/utils/cobrautils/flagsets" + ocmlog "ocm.software/ocm/api/utils/logging" common "ocm.software/ocm/api/utils/misc" "ocm.software/ocm/api/utils/runtime" ) @@ -157,42 +158,47 @@ func (t *DefaultInputType) ApplyConfig(opts flagsets.ConfigOptions, config flags type InputTypeScheme interface { runtime.Scheme[InputSpec, InputType] - ConfigTypeSetConfigProvider() flagsets.ConfigTypeOptionSetConfigProvider - flagsets.ConfigProvider + Copy() InputTypeScheme + + CreateConfigTypeSetConfigProvider() flagsets.ConfigTypeOptionSetConfigProvider GetInputType(name string) InputType Register(atype InputType) - GetInputSpecFor(opts flagsets.ConfigOptions) (InputSpec, error) + GetInputSpecFor(flagsets.Config) (InputSpec, error) DecodeInputSpec(data []byte, unmarshaler runtime.Unmarshaler) (InputSpec, error) CreateInputSpec(obj runtime.TypedObject) (InputSpec, error) } type inputTypeScheme struct { runtime.Scheme[InputSpec, InputType] - optionTypes flagsets.ConfigTypeOptionSetConfigProvider } -func NewInputTypeScheme(defaultRepoDecoder runtime.TypedObjectDecoder[InputSpec]) InputTypeScheme { - scheme := runtime.MustNewDefaultScheme[InputSpec, InputType](&UnknownInputSpec{}, false, defaultRepoDecoder) - prov := flagsets.NewTypedConfigProvider("input", "blob input specification", "inputType") - prov.AddGroups("Input Specification Options") - return &inputTypeScheme{scheme, prov} -} +func NewInputTypeScheme(defaultRepoDecoder runtime.TypedObjectDecoder[InputSpec], base ...InputTypeScheme) InputTypeScheme { + var b runtime.Scheme[InputSpec, InputType] = utils.Optional(base...) -func (t *inputTypeScheme) ConfigTypeSetConfigProvider() flagsets.ConfigTypeOptionSetConfigProvider { - return t.optionTypes + scheme := runtime.MustNewDefaultScheme[InputSpec, InputType](&UnknownInputSpec{}, false, defaultRepoDecoder, b) + return &inputTypeScheme{scheme} } -func (t *inputTypeScheme) CreateOptions() flagsets.ConfigOptions { - return t.optionTypes.CreateOptions() +func (t *inputTypeScheme) Copy() InputTypeScheme { + scheme := runtime.CopyScheme(t.Scheme) + return &inputTypeScheme{scheme} } -func (t *inputTypeScheme) GetInputSpecFor(opts flagsets.ConfigOptions) (InputSpec, error) { - cfg, err := t.GetConfigFor(opts) - if err != nil { - return nil, err +func (t *inputTypeScheme) CreateConfigTypeSetConfigProvider() flagsets.ConfigTypeOptionSetConfigProvider { + prov := flagsets.NewTypedConfigProvider("input", "blob input specification", "inputType") + prov.AddGroups("Input Specification Options") + for _, p := range t.KnownTypes() { + err := prov.AddTypeSet(p.ConfigOptionTypeSetHandler()) + if err != nil { + ocmlog.Logger(REALM).LogError(err, "cannot compose type CLI options", "type", p.GetType()) + } } + return prov +} + +func (t *inputTypeScheme) GetInputSpecFor(cfg flagsets.Config) (InputSpec, error) { data, err := json.Marshal(cfg) if err != nil { return nil, err @@ -200,10 +206,6 @@ func (t *inputTypeScheme) GetInputSpecFor(opts flagsets.ConfigOptions) (InputSpe return t.DecodeInputSpec(data, runtime.DefaultJSONEncoding) } -func (t *inputTypeScheme) GetConfigFor(opts flagsets.ConfigOptions) (flagsets.Config, error) { - return t.optionTypes.GetConfigFor(opts) -} - func (t *inputTypeScheme) GetInputType(name string) InputType { d := t.GetDecoder(name) if d == nil { @@ -217,7 +219,6 @@ func (t *inputTypeScheme) Register(rtype InputType) { return } t.RegisterByDecoder(rtype.GetType(), rtype) - t.optionTypes.AddTypeSet(rtype.ConfigOptionTypeSetHandler()) } func (t *inputTypeScheme) DecodeInputSpec(data []byte, unmarshaler runtime.Unmarshaler) (InputSpec, error) { diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/inputtype_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/inputtype_test.go index a124ee0a33..73f72e4cb8 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/inputtype_test.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/inputtype_test.go @@ -5,7 +5,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "ocm.software/ocm/api/utils/mime" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/types/file" diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/logging.go b/cmds/ocm/commands/ocmcmds/common/inputs/logging.go new file mode 100644 index 0000000000..76aa65a28b --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/logging.go @@ -0,0 +1,7 @@ +package inputs + +import ( + ocmlog "ocm.software/ocm/api/utils/logging" +) + +var REALM = ocmlog.DefineSubRealm("OCM Input Handling", "inputs") diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/options/init.go b/cmds/ocm/commands/ocmcmds/common/inputs/options/init.go new file mode 100644 index 0000000000..387ff5c1f3 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/options/init.go @@ -0,0 +1,12 @@ +package options + +import ( + "ocm.software/ocm/api/utils/cobrautils/flagsets" +) + +var DefaultRegistry = flagsets.SetBaseTypes(flagsets.NewConfigOptionTypeRegistry()) + +func RegisterOption(o flagsets.ConfigOptionType) flagsets.ConfigOptionType { + DefaultRegistry.RegisterOptionType(o) + return o +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/options/standard.go b/cmds/ocm/commands/ocmcmds/common/inputs/options/standard.go index 7ed29a25f3..5fcc6e6315 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/options/standard.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/options/standard.go @@ -6,73 +6,73 @@ import ( ) var ( - HintOption = options.HintOption - MediaTypeOption = options.MediatypeOption + HintOption = RegisterOption(options.HintOption) + MediatypeOption = RegisterOption(options.MediatypeOption) - URLOption = options.URLOption - HTTPHeaderOption = options.HTTPHeaderOption - HTTPVerbOption = options.HTTPVerbOption - HTTPBodyOption = options.HTTPBodyOption - HTTPRedirectOption = options.HTTPRedirectOption + URLOption = RegisterOption(options.URLOption) + HTTPHeaderOption = RegisterOption(options.HTTPHeaderOption) + HTTPVerbOption = RegisterOption(options.HTTPVerbOption) + HTTPBodyOption = RegisterOption(options.HTTPBodyOption) + HTTPRedirectOption = RegisterOption(options.HTTPRedirectOption) - GroupOption = options.GroupOption - ArtifactOption = options.ArtifactOption - ClassifierOption = options.ClassifierOption - ExtensionOption = options.ExtensionOption + GroupOption = RegisterOption(options.GroupOption) + ArtifactOption = RegisterOption(options.ArtifactOption) + ClassifierOption = RegisterOption(options.ClassifierOption) + ExtensionOption = RegisterOption(options.ExtensionOption) - RegistryOption = options.NPMRegistryOption - PackageOption = options.NPMPackageOption - PackageVersionOption = options.NPMVersionOption + RegistryOption = RegisterOption(options.NPMRegistryOption) + PackageOption = RegisterOption(options.NPMPackageOption) + PackageVersionOption = RegisterOption(options.NPMVersionOption) - IdentityPathOption = options.IdentityPathOption + IdentityPathOption = RegisterOption(options.IdentityPathOption) ) // string options. var ( - VersionOption = flagsets.NewStringOptionType("inputVersion", "version info for inputs") - TextOption = flagsets.NewStringOptionType("inputText", "utf8 text") - HelmRepositoryOption = flagsets.NewStringOptionType("inputHelmRepository", "helm repository base URL") + VersionOption = RegisterOption(flagsets.NewStringOptionType("inputVersion", "version info for inputs")) + TextOption = RegisterOption(flagsets.NewStringOptionType("inputText", "utf8 text")) + HelmRepositoryOption = RegisterOption(flagsets.NewStringOptionType("inputHelmRepository", "helm repository base URL")) ) var ( - VariantsOption = flagsets.NewStringArrayOptionType("inputVariants", "(platform) variants for inputs") - PlatformsOption = flagsets.NewStringArrayOptionType("inputPlatforms", "input filter for image platforms ([os]/[architecture])") + VariantsOption = RegisterOption(flagsets.NewStringArrayOptionType("inputVariants", "(platform) variants for inputs")) + PlatformsOption = RegisterOption(flagsets.NewStringArrayOptionType("inputPlatforms", "input filter for image platforms ([os]/[architecture])")) ) // path options. var ( - PathOption = flagsets.NewPathOptionType("inputPath", "path field for input") + PathOption = RegisterOption(flagsets.NewStringOptionType("inputPath", "path field for input")) ) var ( - IncludeOption = flagsets.NewPathArrayOptionType("inputIncludes", "includes (path) for inputs") - ExcludeOption = flagsets.NewPathArrayOptionType("inputExcludes", "excludes (path) for inputs") - LibrariesOption = flagsets.NewPathArrayOptionType("inputLibraries", "library path for inputs") + IncludeOption = RegisterOption(flagsets.NewStringArrayOptionType("inputIncludes", "includes (path) for inputs")) + ExcludeOption = RegisterOption(flagsets.NewStringArrayOptionType("inputExcludes", "excludes (path) for inputs")) + LibrariesOption = RegisterOption(flagsets.NewStringArrayOptionType("inputLibraries", "library path for inputs")) ) // boolean options. var ( - CompressOption = flagsets.NewBoolOptionType("inputCompress", "compress option for input") - PreserveDirOption = flagsets.NewBoolOptionType("inputPreserveDir", "preserve directory in archive for inputs") - FollowSymlinksOption = flagsets.NewBoolOptionType("inputFollowSymlinks", "follow symbolic links during archive creation for inputs") + CompressOption = RegisterOption(flagsets.NewBoolOptionType("inputCompress", "compress option for input")) + PreserveDirOption = RegisterOption(flagsets.NewBoolOptionType("inputPreserveDir", "preserve directory in archive for inputs")) + FollowSymlinksOption = RegisterOption(flagsets.NewBoolOptionType("inputFollowSymlinks", "follow symbolic links during archive creation for inputs")) ) // data options. var ( - DataOption = flagsets.NewBytesOptionType("inputData", "data (string, !!string or !") + DataOption = RegisterOption(flagsets.NewBytesOptionType("inputData", "data (string, !!string or !")) ) // yaml/json options. var ( - YAMLOption = flagsets.NewYAMLOptionType("inputYaml", "YAML formatted text") - JSONOption = flagsets.NewYAMLOptionType("inputJson", "JSON formatted text") - FormattedJSONOption = flagsets.NewYAMLOptionType("inputFormattedJson", "JSON formatted text") + YAMLOption = RegisterOption(flagsets.NewYAMLOptionType("inputYaml", "YAML formatted text")) + JSONOption = RegisterOption(flagsets.NewYAMLOptionType("inputJson", "JSON formatted text")) + FormattedJSONOption = RegisterOption(flagsets.NewYAMLOptionType("inputFormattedJson", "JSON formatted text")) ) var ( - ValuesOption = flagsets.NewValueMapYAMLOptionType("inputValues", "YAML based generic values for inputs") - ComponentOption = flagsets.NewStringOptionType("inputComponent", "component name") + ValuesOption = RegisterOption(flagsets.NewValueMapYAMLOptionType("inputValues", "YAML based generic values for inputs")) + ComponentOption = RegisterOption(flagsets.NewStringOptionType("inputComponent", "component name")) ) // RepositoryOption sets the repository or registry for an input. -var RepositoryOption = flagsets.NewStringOptionType("inputRepository", "repository or registry for inputs") +var RepositoryOption = RegisterOption(flagsets.NewStringOptionType("inputRepository", "repository or registry for inputs")) diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/setup.go b/cmds/ocm/commands/ocmcmds/common/inputs/setup.go new file mode 100644 index 0000000000..c7afdfe374 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/setup.go @@ -0,0 +1,27 @@ +package inputs + +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, NewInputTypeScheme(nil, DefaultInputTypeScheme)) + case datacontext.MODE_CONFIGURED: + SetFor(octx, DefaultInputTypeScheme.Copy()) + case datacontext.MODE_INITIAL: + SetFor(octx, NewInputTypeScheme(nil)) + } + } +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/binary/inputtype_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/binary/inputtype_test.go index eb381a2ded..0566289430 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/binary/inputtype_test.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/binary/inputtype_test.go @@ -18,7 +18,7 @@ var _ = Describe("Input Type", func() { It("simple string decode", func() { env.Set(options.CompressOption, "true") - env.Set(options.MediaTypeOption, "media") + env.Set(options.MediatypeOption, "media") env.Set(options.DataOption, "!stringdata") env.Check(&Spec{ Data: runtime.Binary("stringdata"), @@ -28,7 +28,7 @@ var _ = Describe("Input Type", func() { It("binary decode", func() { env.Set(options.CompressOption, "true") - env.Set(options.MediaTypeOption, "media") + env.Set(options.MediatypeOption, "media") env.Set(options.DataOption, "IXN0cmluZ2RhdGE=") env.Check(&Spec{ Data: runtime.Binary("!stringdata"), diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/directory/inputtype_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/directory/inputtype_test.go index 6679c7905b..bbc2aa7065 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/directory/inputtype_test.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/directory/inputtype_test.go @@ -21,7 +21,7 @@ var _ = Describe("Input Type", func() { It("simple decode", func() { env.Set(options.PathOption, "mypath") env.Set(options.CompressOption, "true") - env.Set(options.MediaTypeOption, "media") + env.Set(options.MediatypeOption, "media") env.Set(options.PreserveDirOption, "false") env.Set(options.FollowSymlinksOption, "true") env.Set(options.IncludeOption, "x") diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/cli.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/cli.go index 043fd1f1f4..9e1d6a3e00 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/cli.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/cli.go @@ -6,8 +6,8 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/options" ) -func ConfigHandler() flagsets.ConfigOptionTypeSetHandler { - return cpi.NewMediaFileSpecOptionType(TYPE, AddConfig, +func ConfigHandler(t string) flagsets.ConfigOptionTypeSetHandler { + return cpi.NewMediaFileSpecOptionType(t, AddConfig, options.PathOption, options.HintOption, options.PlatformsOption) } diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/input_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/input_test.go index 27d6350d16..be706dad5d 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/input_test.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/input_test.go @@ -120,23 +120,24 @@ var _ = Describe("Test Environment", func() { Context("inputs", func() { BeforeEach(func() { flags = &pflag.FlagSet{} - opts = inputs.DefaultInputTypeScheme.CreateOptions() + opts = inputs.DefaultInputTypeScheme.CreateConfigTypeSetConfigProvider().CreateOptions() opts.AddFlags(flags) cfg = flagsets.Config{} }) It("input type", func() { fmt.Printf("input option names: %+v\n", opts.Names()) + prov := inputs.DefaultInputTypeScheme.CreateConfigTypeSetConfigProvider() MustBeSuccessful(flagsets.ParseOptionsFor(flags, - flagsets.OptionSpec(inputs.DefaultInputTypeScheme.ConfigTypeSetConfigProvider().GetTypeOptionType(), me.TYPE), + flagsets.OptionSpec(prov.GetTypeOptionType(), me.TYPE), flagsets.OptionSpec(options.PathOption, "ghcr.io/open-component-model/image:v1.0"), flagsets.OptionSpec(options.PlatformsOption, "linux/amd64"), flagsets.OptionSpec(options.PlatformsOption, "/arm64"), )) - cfg := Must(inputs.DefaultInputTypeScheme.GetConfigFor(opts)) + cfg := Must(prov.GetConfigFor(opts)) fmt.Printf("selected input options: %+v\n", cfg) - spec := Must(inputs.DefaultInputTypeScheme.GetInputSpecFor(opts)) + spec := Must(inputs.DefaultInputTypeScheme.GetInputSpecFor(cfg)) Expect(spec).To(Equal(me.New("ghcr.io/open-component-model/image:v1.0", "linux/amd64", "/arm64"))) }) }) diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/type.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/type.go index 4c5ae34738..13682a9093 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/type.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/ociartifact/type.go @@ -10,8 +10,8 @@ const ( ) func init() { - inputs.DefaultInputTypeScheme.Register(inputs.NewInputType(TYPE, &Spec{}, usage, ConfigHandler())) - inputs.DefaultInputTypeScheme.Register(inputs.NewInputType(LEGACY_TYPE, &Spec{}, legacy_usage, ConfigHandler())) + inputs.DefaultInputTypeScheme.Register(inputs.NewInputType(TYPE, &Spec{}, usage, ConfigHandler(TYPE))) + inputs.DefaultInputTypeScheme.Register(inputs.NewInputType(LEGACY_TYPE, &Spec{}, legacy_usage, ConfigHandler(LEGACY_TYPE))) } const legacy_usage = ` diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin.go new file mode 100644 index 0000000000..15e49e118f --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin.go @@ -0,0 +1,98 @@ +package plugin + +import ( + "bytes" + "encoding/json" + "sync" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/credentials/identity/hostpath" + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/ocm/plugin/descriptor" +) + +type plug = plugin.Plugin + +// PluginHandler is a shared object between the GetAccess implementation and the Spec implementation. The +// object knows the actual plugin and can therefore forward the method calls to corresponding cli commands. +type PluginHandler struct { + lock sync.Mutex + plug + + // cached info + access *access + err error + orig []byte +} + +func NewPluginHandler(p plugin.Plugin) *PluginHandler { + return &PluginHandler{plug: p} +} + +func (p *PluginHandler) GetAccess(spec *Spec, ctx cpi.Context) (*access, error) { + raw, err := spec.GetRaw() + if err != nil { + return nil, errors.Wrapf(err, "cannot marshal input specification") + } + p.lock.Lock() + defer p.lock.Unlock() + + if p.access != nil || p.err != nil { + if bytes.Equal(raw, p.orig) { + return p.access, p.err + } + } + mspec := p.GetInputTypeDescriptor(spec.GetKind(), spec.GetVersion()) + if mspec == nil { + return nil, errors.ErrNotFound(descriptor.KIND_INPUTTYPE, spec.GetType(), descriptor.KIND_PLUGIN, p.Name()) + } + + info, err := p.plug.ValidateInputSpec(raw) + p.err = err + if err != nil { + return nil, err + } + p.access = newAccess(p, spec, ctx, info) + + creddata, err := p.getCredentialData(info, ctx) + if err != nil { + return nil, err + } + p.access.creds = creddata + + return p.access, nil +} + +func (p *PluginHandler) getCredentialData(info *plugin.InputSpecInfo, ctx cpi.Context) (json.RawMessage, error) { + var ( + err error + creds credentials.Credentials + ) + + if len(info.ConsumerId) > 0 { + creds, err = credentials.CredentialsForConsumer(ctx, info.ConsumerId, hostpath.IdentityMatcher(info.ConsumerId.Type())) + if err != nil { + return nil, err + } + } + + var creddata json.RawMessage + if creds != nil { + creddata, err = json.Marshal(creds) + if err != nil { + return nil, err + } + } + return creddata, nil +} + +func (p *PluginHandler) Describe(spec *Spec, ctx cpi.Context) string { + acc, err := p.GetAccess(spec, ctx) + if err != nil { + return err.Error() + } + return acc.info.Short +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin_test.go new file mode 100644 index 0000000000..b237edbc86 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin_test.go @@ -0,0 +1,140 @@ +package plugin_test + +import ( + "os" + + "github.com/mandelsoft/goutils/sliceutils" + . "github.com/mandelsoft/goutils/testutils" + "github.com/mandelsoft/goutils/transformer" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/pflag" + "ocm.software/ocm/api/ocm/extensions/accessmethods/options" + . "ocm.software/ocm/api/ocm/plugin/testutils" + "ocm.software/ocm/api/utils/cobrautils/flagsets" + . "ocm.software/ocm/cmds/ocm/testhelper" + + "github.com/mandelsoft/filepath/pkg/filepath" + + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/api/ocm/plugin/plugins" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin" +) + +const PLUGIN = "input" + +const TESTDATA = "this is some test data\n" + +var _ = Describe("Input Command Test Environment", func() { + Context("plugin execution", func() { + var env *TestEnv + var plugindir TempPluginDir + var registry plugins.Set + + BeforeEach(func() { + env = NewTestEnv(TestData()) + plugindir = Must(ConfigureTestPlugins(env, "testdata/plugins")) + registry = plugincacheattr.Get(env) + }) + + AfterEach(func() { + plugindir.Cleanup() + env.Cleanup() + }) + + It("loads plugin", func() { + // Expect(registration.RegisterExtensions(env)).To(Succeed()) + p := registry.Get(PLUGIN) + Expect(p).NotTo(BeNil()) + Expect(p.Error()).To(Equal("")) + }) + + It("gets blob", func() { + p := registry.Get(PLUGIN) + t := plugin.NewType("demo", p, &p.GetDescriptor().Inputs[0]) + + file := Must(os.CreateTemp("", "input*")) + defer os.Remove(file.Name()) + Must(file.Write([]byte(TESTDATA))) + file.Close() + spec := ` +type: demo +path: ` + filepath.Base(file.Name()) + ` +mediaType: plain/text +` + is := Must(t.Decode([]byte(spec), runtime.DefaultYAMLEncoding)) + Expect(is.GetType()).To(Equal("demo")) + + ctx := inputs.NewContext(env.CLIContext(), nil, nil) + blob, hint := Must2(is.GetBlob(ctx, inputs.InputResourceInfo{})) + defer blob.Close() + Expect(hint).To(Equal(filepath.Base(file.Name()))) + data := Must(blob.Get()) + Expect(string(data)).To(Equal(TESTDATA)) + }) + + It("gets input", func() { + scheme := inputs.For(env.CLIContext()) + plugin.RegisterPlugins(env.CLIContext()) + + file := Must(os.CreateTemp("", "input*")) + defer os.Remove(file.Name()) + Must(file.Write([]byte(TESTDATA))) + file.Close() + spec := ` +type: demo +path: ` + filepath.Base(file.Name()) + ` +mediaType: plain/text +` + + is := Must(scheme.DecodeInputSpec([]byte(spec), runtime.DefaultYAMLEncoding)) + Expect(is.GetType()).To(Equal("demo")) + + ctx := inputs.NewContext(env.CLIContext(), nil, nil) + blob, hint := Must2(is.GetBlob(ctx, inputs.InputResourceInfo{})) + defer blob.Close() + Expect(hint).To(Equal(filepath.Base(file.Name()))) + data := Must(blob.Get()) + Expect(string(data)).To(Equal(TESTDATA)) + }) + + It("handles input options", func() { + scheme := inputs.For(env.CLIContext()) + plugin.RegisterPlugins(env.CLIContext()) + + it := scheme.GetInputType("demo") + Expect(it).NotTo(BeNil()) + + h := it.ConfigOptionTypeSetHandler() + Expect(h).NotTo(BeNil()) + Expect(h.GetName()).To(Equal("demo")) + + ot := h.OptionTypes() + Expect(len(ot)).To(Equal(2)) + + opts := h.CreateOptions() + Expect(sliceutils.Transform(opts.Options(), transformer.GetName[flagsets.Option, string])).To(ConsistOf( + "mediaType", "inputPath")) + + fs := &pflag.FlagSet{} + fs.SortFlags = true + opts.AddFlags(fs) + + Expect("\n" + fs.FlagUsages()).To(Equal(` + --inputPath string path field for input + --mediaType string media type for artifact blob representation +`)) + + MustBeSuccessful(fs.Parse([]string{"--inputPath", "filepath", "--" + options.MediatypeOption.GetName(), "yaml"})) + + cfg := flagsets.Config{} + MustBeSuccessful(h.ApplyConfig(opts, cfg)) + Expect(cfg).To(YAMLEqual(` +mediaType: yaml +path: filepath +`)) + }) + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/registration.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/registration.go new file mode 100644 index 0000000000..437ca30c78 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/registration.go @@ -0,0 +1,25 @@ +package plugin + +import ( + "github.com/mandelsoft/goutils/generics" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs" +) + +func RegisterPlugins(ctx clictx.Context) { + scheme := inputs.For(ctx) + + plugins := plugincacheattr.Get(ctx) + for _, n := range plugins.PluginNames() { + p := plugins.Get(n) + if !p.IsValid() { + continue + } + for _, d := range p.GetDescriptor().Inputs { + t := NewType(d.Name, p, generics.Pointer(d)) + scheme.Register(t) + } + } +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/spec.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/spec.go new file mode 100644 index 0000000000..4f62546214 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/spec.go @@ -0,0 +1,101 @@ +package plugin + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/errors" + "k8s.io/apimachinery/pkg/util/validation/field" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/credentials/identity/hostpath" + "ocm.software/ocm/api/ocm" + cpi "ocm.software/ocm/api/ocm/cpi/accspeccpi" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs" +) + +type Spec struct { + runtime.UnstructuredVersionedTypedObject `json:",inline"` + handler *PluginHandler +} + +var _ inputs.InputSpec = &Spec{} + +func (s *Spec) Validate(fldPath *field.Path, ctx inputs.Context, inputFilePath string) field.ErrorList { + _, err := s.handler.GetAccess(s, ctx.OCMContext()) + if err != nil { + return field.ErrorList{field.Invalid(fldPath, nil, err.Error())} + } + return nil +} + +func (s *Spec) GetBlob(ctx inputs.Context, info inputs.InputResourceInfo) (blobaccess.BlobAccess, string, error) { + acc, err := s.handler.GetAccess(s, ctx.OCMContext()) + if err != nil { + return nil, "", err + } + dir, err := inputs.GetBaseDir(ctx.FileSystem(), info.InputFilePath) + if err != nil { + dir = "." + } + return acc.GetBlob(dir) +} + +func (s *Spec) GetInputVersion(ctx inputs.Context) string { + return "" +} + +func (s *Spec) Describe(ctx cpi.Context) string { + return s.handler.Describe(s, ctx) +} + +func (s *Spec) Handler() *PluginHandler { + return s.handler +} + +//////////////////////////////////////////////////////////////////////////////// + +type access struct { + ctx ocm.Context + + handler *PluginHandler + spec *Spec + info *ppi.InputSpecInfo + creds json.RawMessage +} + +func newAccess(p *PluginHandler, spec *Spec, ctx ocm.Context, info *ppi.InputSpecInfo) *access { + return &access{ + ctx: ctx, + handler: p, + spec: spec, + info: info, + } +} + +func (m *access) MimeType() string { + return m.info.MediaType +} + +func (m *access) GetBlob(dir string) (blobaccess.BlobAccess, string, error) { + spec, err := json.Marshal(m.spec) + if err != nil { + return nil, "", errors.Wrapf(err, "cannot marshal input spec") + } + return accessobj.CachedBlobAccessForWriter(m.ctx, m.MimeType(), plugin.NewInputDataWriter(m.handler.plug, dir, m.creds, spec)), m.info.Hint, nil +} + +func (m *access) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity { + if len(m.info.ConsumerId) == 0 { + return nil + } + return m.info.ConsumerId +} + +func (m *access) GetIdentityMatcher() string { + return hostpath.IDENTITY_TYPE +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/suite_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/suite_test.go new file mode 100644 index 0000000000..8c8dfdc441 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/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, "Input Plugin Handler Test Suite") +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/app/app.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/app/app.go new file mode 100644 index 0000000000..2e39d1ecef --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/app/app.go @@ -0,0 +1,29 @@ +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/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/inputhandlers" +) + +func New() (ppi.Plugin, error) { + p := ppi.NewPlugin("input", version.Get().String()) + + p.SetShort("fake input plugin") + p.SetLong("providing fake input") + + err := p.RegisterInputType(inputhandlers.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/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/inputhandlers/demo.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/inputhandlers/demo.go new file mode 100644 index 0000000000..083199dd35 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/inputhandlers/demo.go @@ -0,0 +1,103 @@ +package inputhandlers + +import ( + out "fmt" + "io" + "os" + "strings" + + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/credentials/cpi" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/tech/oci/identity" + "ocm.software/ocm/api/utils/cobrautils/flagsets" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/cmds/demoplugin/common" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/options" +) + +const ( + NAME = "demo" + VERSION = "v1" +) + +type InputSpec struct { + runtime.ObjectVersionedType `json:",inline"` + + Path string `json:"path"` + MediaType string `json:"mediaType,omitempty"` +} + +type InputType struct { + ppi.InputTypeBase +} + +var _ ppi.InputType = (*InputType)(nil) + +func New() ppi.InputType { + return &InputType{ + InputTypeBase: ppi.MustNewInputTypeBase(NAME, &InputSpec{}, "demo access to temp files", ""), + } +} + +func (a *InputType) Options() []flagsets.ConfigOptionType { + return []flagsets.ConfigOptionType{ + options.MediatypeOption, + options.PathOption, + } +} + +func (a *InputType) Decode(data []byte, unmarshaler runtime.Unmarshaler) (runtime.TypedObject, error) { + if unmarshaler == nil { + unmarshaler = runtime.DefaultYAMLEncoding + } + var spec InputSpec + err := unmarshaler.Unmarshal(data, &spec) + if err != nil { + return nil, err + } + return &spec, nil +} + +func (a *InputType) ValidateSpecification(p ppi.Plugin, dir string, spec ppi.InputSpec) (*ppi.InputSpecInfo, error) { + var info ppi.InputSpecInfo + + my := spec.(*InputSpec) + + if my.Path == "" { + return nil, out.Errorf("path not specified") + } + if strings.HasPrefix(my.Path, "/") { + return nil, out.Errorf("path must be relative (%s)", my.Path) + } + if my.MediaType == "" { + return nil, out.Errorf("mediaType not specified") + } + info.MediaType = my.MediaType + info.ConsumerId = credentials.ConsumerIdentity{ + cpi.ID_TYPE: common.CONSUMER_TYPE, + identity.ID_HOSTNAME: "localhost", + identity.ID_PATHPREFIX: my.Path, + } + info.Short = "temp file " + my.Path + info.Hint = my.Path + + return &info, nil +} + +func (a *InputType) ComposeSpecification(p ppi.Plugin, opts ppi.Config, config ppi.Config) error { + list := errors.ErrListf("configuring options") + list.Add(flagsets.AddFieldByOptionP(opts, options.PathOption, config, "path")) + list.Add(flagsets.AddFieldByOptionP(opts, options.MediatypeOption, config, "mediaType")) + return list.Result() +} + +func (a *InputType) Reader(p ppi.Plugin, dir string, spec ppi.InputSpec, creds credentials.Credentials) (io.ReadCloser, error) { + my := spec.(*InputSpec) + + root := os.TempDir() + return os.Open(filepath.Join(root, my.Path)) +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/main.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/main.go new file mode 100644 index 0000000000..3d9d08bd8d --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/app" +) + +func main() { + err := app.Run(os.Args[1:]) + if err != nil { + os.Exit(1) + } +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugins/input b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugins/input new file mode 100755 index 0000000000..97d52eddbb --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugins/input @@ -0,0 +1,2 @@ +#!/bin/bash +go run testdata/plugin/main.go "$@" \ No newline at end of file diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/type.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/type.go new file mode 100644 index 0000000000..732e057387 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/type.go @@ -0,0 +1,68 @@ +package plugin + +import ( + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/utils/cobrautils/flagsets" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs/options" +) + +type inputType struct { + inputs.InputType + plug plugin.Plugin + cliopts flagsets.ConfigOptionTypeSet +} + +var _ inputs.InputType = (*inputType)(nil) + +func NewType(name string, p plugin.Plugin, desc *plugin.InputTypeDescriptor) inputs.InputType { + t := &inputType{ + plug: p, + } + + cfghdlr := flagsets.NewConfigOptionTypeSetHandler(name, t.AddConfig) + for _, o := range desc.CLIOptions { + var opt flagsets.ConfigOptionType + if o.Type == "" { + opt = options.DefaultRegistry.GetOptionType(o.Name) + if opt == nil { + p.Context().Logger(plugin.TAG).Warn("unknown option", "plugin", p.Name(), "inputtype", name, "option", o.Name) + } + } else { + var err error + opt, err = options.DefaultRegistry.CreateOptionType(o.Type, o.Name, o.Description) + if err != nil { + p.Context().Logger(plugin.TAG).Warn("invalid option", "plugin", p.Name(), "inputtype", name, "option", o.Name, "error", err.Error()) + } + } + if opt != nil { + cfghdlr.AddOptionType(opt) + } + } + if cfghdlr.Size() > 0 { + t.cliopts = cfghdlr + } + + usage := desc.Description + format := desc.Format + if format != "" { + usage += "\n" + format + } + t.InputType = inputs.NewInputType(name, &Spec{}, usage, cfghdlr) + return t +} + +func (t *inputType) Decode(data []byte, unmarshaler runtime.Unmarshaler) (inputs.InputSpec, error) { + spec, err := t.InputType.Decode(data, unmarshaler) + if err != nil { + return nil, err + } + spec.(*Spec).handler = NewPluginHandler(t.plug) + return spec, nil +} + +func (t *inputType) AddConfig(opts flagsets.ConfigOptions, cfg flagsets.Config) error { + opts = opts.FilterBy(t.cliopts.HasOptionType) + return t.plug.ComposeInputSpec(t.GetType(), opts, cfg) +} diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/utf8/inputtype_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/utf8/inputtype_test.go index 1afad536f4..8feb3f38b9 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/utf8/inputtype_test.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/utf8/inputtype_test.go @@ -22,7 +22,7 @@ var _ = Describe("Input Type", func() { It("simple string decode", func() { env.Set(options.CompressOption, "true") - env.Set(options.MediaTypeOption, "media") + env.Set(options.MediatypeOption, "media") env.Set(options.TextOption, "stringdata") env.Check(&Spec{ Text: "stringdata", @@ -32,7 +32,7 @@ var _ = Describe("Input Type", func() { It("simple json decode", func() { env.Set(options.CompressOption, "true") - env.Set(options.MediaTypeOption, "media") + env.Set(options.MediatypeOption, "media") env.Set(options.JSONOption, `field: value`) env.Check(&Spec{ Json: []byte(`{"field":"value"}`), @@ -42,7 +42,7 @@ var _ = Describe("Input Type", func() { It("simple formatted json decode", func() { env.Set(options.CompressOption, "true") - env.Set(options.MediaTypeOption, "media") + env.Set(options.MediatypeOption, "media") env.Set(options.FormattedJSONOption, `field: value`) env.Check(&Spec{ FormattedJson: []byte(`{"field":"value"}`), @@ -52,7 +52,7 @@ var _ = Describe("Input Type", func() { It("simple yaml decode", func() { env.Set(options.CompressOption, "true") - env.Set(options.MediaTypeOption, "media") + env.Set(options.MediatypeOption, "media") env.Set(options.YAMLOption, `field: value`) env.Check(&Spec{ Yaml: []byte(`{"field":"value"}`), diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/cli.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/cli.go index e8e16f098f..73cb7a1bc8 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/cli.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/cli.go @@ -9,7 +9,7 @@ func ConfigHandler() flagsets.ConfigOptionTypeSetHandler { return flagsets.NewConfigOptionTypeSetHandler( TYPE, AddConfig, options.URLOption, - options.MediaTypeOption, + options.MediatypeOption, options.HTTPHeaderOption, options.HTTPVerbOption, options.HTTPBodyOption, @@ -19,7 +19,7 @@ func ConfigHandler() flagsets.ConfigOptionTypeSetHandler { func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error { flagsets.AddFieldByOptionP(opts, options.URLOption, config, "url") - flagsets.AddFieldByOptionP(opts, options.MediaTypeOption, config, "mediaType") + flagsets.AddFieldByOptionP(opts, options.MediatypeOption, config, "mediaType") flagsets.AddFieldByOptionP(opts, options.HTTPHeaderOption, config, "header") flagsets.AddFieldByOptionP(opts, options.HTTPVerbOption, config, "verb") flagsets.AddFieldByOptionP(opts, options.HTTPBodyOption, config, "body") diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/input_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/input_test.go index 6c20995db8..17c1a2f415 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/input_test.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/wget/input_test.go @@ -21,7 +21,7 @@ var _ = Describe("Input Type", func() { It("simple decode", func() { env.Set(options.URLOption, "https://example.com/test") - env.Set(options.MediaTypeOption, mime.MIME_TEXT) + env.Set(options.MediatypeOption, mime.MIME_TEXT) env.Set(options.HTTPHeaderOption, "Host: developer.mozilla.org") env.Set(options.HTTPHeaderOption, "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0") env.Set(options.HTTPHeaderOption, "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8") 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/common/resources.go b/cmds/ocm/commands/ocmcmds/common/resources.go index 63c1d63c44..035dba5bff 100644 --- a/cmds/ocm/commands/ocmcmds/common/resources.go +++ b/cmds/ocm/commands/ocmcmds/common/resources.go @@ -266,7 +266,7 @@ func (a *ContentResourceSpecificationsProvider) AddFlags(fs *pflag.FlagSet) { a.ElementMetaDataSpecificationsProvider.AddFlags(fs) a.accprov = a.ctx.OCMContext().AccessMethods().CreateConfigTypeSetConfigProvider() - inptypes := inputs.For(a.ctx).ConfigTypeSetConfigProvider() + inptypes := inputs.For(a.ctx).CreateConfigTypeSetConfigProvider() set := flagsets.NewConfigOptionTypeSet("resources") set.AddAll(a.accprov) @@ -301,7 +301,7 @@ func (a *ContentResourceSpecificationsProvider) Complete() error { unique := a.options.FilterBy(flagsets.Not(a.shared.HasOptionType)) aopts := unique.FilterBy(a.accprov.HasOptionType) - iopts := unique.FilterBy(inputs.For(a.ctx).ConfigTypeSetConfigProvider().HasOptionType) + iopts := unique.FilterBy(inputs.For(a.ctx).CreateConfigTypeSetConfigProvider().HasOptionType) if !a.options.Changed(a.contentFlags...) { return fmt.Errorf("one of %v is required", flagsets.AddPrefix("--", a.contentFlags...)) @@ -350,7 +350,7 @@ func (a *ContentResourceSpecificationsProvider) Get() (string, error) { if err != nil { return "", err } - err = a.apply(inputs.For(a.ctx).ConfigTypeSetConfigProvider(), data) + err = a.apply(inputs.For(a.ctx).CreateConfigTypeSetConfigProvider(), data) if err != nil { 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