From 53a808858c505db0d8c07310fe8dc1b4ab2e0b93 Mon Sep 17 00:00:00 2001 From: Uwe Krueger Date: Sun, 6 Oct 2024 17:06:02 +0200 Subject: [PATCH] input plugin handler --- .../extensions/accessmethods/plugin/plugin.go | 2 +- api/ocm/plugin/cache/plugin.go | 24 +++ api/ocm/plugin/interface.go | 2 +- api/ocm/plugin/plugin_input.go | 3 +- api/ocm/plugin/ppi/cmds/input/compose/cmd.go | 2 +- api/ocm/plugin/ppi/cmds/input/validate/cmd.go | 10 +- api/ocm/plugin/ppi/plugin.go | 3 +- api/ocm/plugin/ppi/utils.go | 12 +- api/ocm/plugin/utils.go | 21 ++- api/utils/runtime/scheme.go | 27 ++++ cmds/ocm/app/app.go | 2 + .../ocmcmds/common/inputs/extend_test.go | 120 +++++++++++---- .../ocmcmds/common/inputs/inputtype.go | 9 +- .../commands/ocmcmds/common/inputs/setup.go | 27 ++++ .../common/inputs/types/plugin/plugin.go | 98 ++++++++++++ .../common/inputs/types/plugin/plugin_test.go | 140 ++++++++++++++++++ .../inputs/types/plugin/registration.go | 25 ++++ .../common/inputs/types/plugin/spec.go | 97 ++++++++++++ .../common/inputs/types/plugin/suite_test.go | 13 ++ .../types/plugin/testdata/plugin/app/app.go | 29 ++++ .../testdata/plugin/inputhandlers/demo.go | 105 +++++++++++++ .../types/plugin/testdata/plugin/main.go | 14 ++ .../types/plugin/testdata/plugins/input | 2 + .../common/inputs/types/plugin/type.go | 68 +++++++++ 24 files changed, 800 insertions(+), 55 deletions(-) create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/setup.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/plugin_test.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/registration.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/spec.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/suite_test.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/app/app.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/inputhandlers/demo.go create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/main.go create mode 100755 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugins/input create mode 100644 cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/type.go diff --git a/api/ocm/extensions/accessmethods/plugin/plugin.go b/api/ocm/extensions/accessmethods/plugin/plugin.go index 55ce2f137d..5e70200d57 100644 --- a/api/ocm/extensions/accessmethods/plugin/plugin.go +++ b/api/ocm/extensions/accessmethods/plugin/plugin.go @@ -132,7 +132,7 @@ func (p *PluginHandler) GetMimeType(spec *AccessSpec) string { if err != nil { return "" } - return info.Short + return info.MediaType } func (p *PluginHandler) GetReferenceHint(spec *AccessSpec, cv cpi.ComponentVersionAccess) string { diff --git a/api/ocm/plugin/cache/plugin.go b/api/ocm/plugin/cache/plugin.go index 6b74dde38d..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 diff --git a/api/ocm/plugin/interface.go b/api/ocm/plugin/interface.go index 326fec147e..ce2278495c 100644 --- a/api/ocm/plugin/interface.go +++ b/api/ocm/plugin/interface.go @@ -10,7 +10,7 @@ const ( KIND_PLUGIN = descriptor.KIND_PLUGIN KIND_UPLOADER = descriptor.KIND_UPLOADER KIND_ACCESSMETHOD = descriptor.KIND_ACCESSMETHOD - KIND_INPUTTYÜE = descriptor.KIND_INPUTTYPE + KIND_INPUTTYPE = descriptor.KIND_INPUTTYPE KIND_ACTION = descriptor.KIND_ACTION KIND_TRANSFERHANDLER = descriptor.KIND_TRANSFERHANDLER ) diff --git a/api/ocm/plugin/plugin_input.go b/api/ocm/plugin/plugin_input.go index 3a3eb800e2..6566ca1eb5 100644 --- a/api/ocm/plugin/plugin_input.go +++ b/api/ocm/plugin/plugin_input.go @@ -7,7 +7,6 @@ import ( "github.com/mandelsoft/goutils/errors" "ocm.software/ocm/api/ocm/plugin/ppi" - "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod" "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" @@ -62,7 +61,7 @@ func (p *pluginImpl) ComposeInputSpec(name string, opts flagsets.ConfigOptions, } func (p *pluginImpl) GetInputBlob(w io.Writer, creds, spec json.RawMessage) error { - args := []string{accessmethod.Name, get.Name, string(spec)} + args := []string{input.Name, get.Name, string(spec)} if creds != nil { args = append(args, "--"+get.OptCreds, string(creds)) } diff --git a/api/ocm/plugin/ppi/cmds/input/compose/cmd.go b/api/ocm/plugin/ppi/cmds/input/compose/cmd.go index cda74b0c07..d96040c1f8 100644 --- a/api/ocm/plugin/ppi/cmds/input/compose/cmd.go +++ b/api/ocm/plugin/ppi/cmds/input/compose/cmd.go @@ -6,9 +6,9 @@ import ( "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/extensions/accessmethods/options" + "ocm.software/ocm/api/ocm/plugin/descriptor" "ocm.software/ocm/api/ocm/plugin/ppi" "ocm.software/ocm/api/utils/runtime" ) diff --git a/api/ocm/plugin/ppi/cmds/input/validate/cmd.go b/api/ocm/plugin/ppi/cmds/input/validate/cmd.go index d5aba657e8..827b9ff5ef 100644 --- a/api/ocm/plugin/ppi/cmds/input/validate/cmd.go +++ b/api/ocm/plugin/ppi/cmds/input/validate/cmd.go @@ -8,8 +8,8 @@ import ( "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/errkind" "ocm.software/ocm/api/utils/runtime" ) @@ -82,14 +82,14 @@ type Result struct { } func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { - spec, err := p.DecodeAccessSpecification(opts.Specification) + spec, err := p.DecodeInputSpecification(opts.Specification) if err != nil { - return errors.Wrapf(err, "access specification") + return errors.Wrapf(err, "input specification") } - m := p.GetAccessMethod(runtime.KindVersion(spec.GetType())) + m := p.GetInputType(spec.GetType()) if m == nil { - return errors.ErrUnknown(errkind.KIND_ACCESSMETHOD, spec.GetType()) + return errors.ErrUnknown(descriptor.KIND_INPUTTYPE, spec.GetType()) } info, err := m.ValidateSpecification(p, spec) if err != nil { diff --git a/api/ocm/plugin/ppi/plugin.go b/api/ocm/plugin/ppi/plugin.go index f85fca9543..d05a0776c5 100644 --- a/api/ocm/plugin/ppi/plugin.go +++ b/api/ocm/plugin/ppi/plugin.go @@ -10,6 +10,7 @@ import ( "github.com/mandelsoft/goutils/maputils" "github.com/spf13/cobra" "golang.org/x/exp/slices" + "ocm.software/ocm/api/config" "ocm.software/ocm/api/datacontext/action" metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" @@ -334,7 +335,7 @@ func (p *plugin) RegisterInputType(m InputType) error { }, } p.descriptor.Inputs = append(p.descriptor.Inputs, inp) - p.accessScheme.RegisterByDecoder(m.Name(), m) + p.inputScheme.RegisterByDecoder(m.Name(), m) p.inputs[m.Name()] = m return nil } diff --git a/api/ocm/plugin/ppi/utils.go b/api/ocm/plugin/ppi/utils.go index 88c374a564..c777173cff 100644 --- a/api/ocm/plugin/ppi/utils.go +++ b/api/ocm/plugin/ppi/utils.go @@ -45,10 +45,16 @@ func (b *AccessMethodBase) BlobProviderBase() string { //////////////////////////////////////////////////////////////////////////////// -type InputTypeBase = blobProviderBase +type InputTypeBase struct { + blobProviderBase +} -func MustNewAInputTypeBase(name string, proto InputSpec, desc string, format string) InputTypeBase { - return MustNewBlobProviderBase(name, proto, desc, format) +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 } //////////////////////////////////////////////////////////////////////////////// diff --git a/api/ocm/plugin/utils.go b/api/ocm/plugin/utils.go index f3f15ae8de..bbcb8afd46 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,23 @@ 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, accspec json.RawMessage) *BlobAccessWriter { + return &BlobAccessWriter{creds, accspec, p.Get} } -func (d *AccessDataWriter) WriteTo(w accessio.Writer) (int64, digest.Digest, error) { +func NewInputDataWriter(p Plugin, creds, accspec json.RawMessage) *BlobAccessWriter { + return &BlobAccessWriter{creds, accspec, p.GetInputBlob} +} + +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/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/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/inputs/extend_test.go b/cmds/ocm/commands/ocmcmds/common/inputs/extend_test.go index 0250a622be..eeef9db3d5 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/extend_test.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/extend_test.go @@ -6,6 +6,8 @@ import ( . "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" @@ -21,45 +23,99 @@ import ( var _ = Describe("Input Type Extension Test Environment", func() { var ( - scheme = inputs.NewInputTypeScheme(nil, inputs.DefaultInputTypeScheme) + scheme inputs.InputTypeScheme itype = inputs.NewInputType(TYPE, &Spec{}, "", ConfigHandler()) flags *pflag.FlagSet opts flagsets.ConfigOptions ) - BeforeEach(func() { - scheme.Register(itype) - flags = &pflag.FlagSet{} - opts = scheme.CreateConfigTypeSetConfigProvider().CreateOptions() - opts.AddFlags(flags) + 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"))) + }) }) - 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"))) + }) }) }) diff --git a/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go b/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go index de55d1779b..ec60849a2c 100644 --- a/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go +++ b/cmds/ocm/commands/ocmcmds/common/inputs/inputtype.go @@ -9,13 +9,13 @@ import ( "github.com/mandelsoft/goutils/errors" "github.com/modern-go/reflect2" "k8s.io/apimachinery/pkg/util/validation/field" - ocmlog "ocm.software/ocm/api/utils/logging" clictx "ocm.software/ocm/api/cli" "ocm.software/ocm/api/datacontext" "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" ) @@ -158,6 +158,8 @@ func (t *DefaultInputType) ApplyConfig(opts flagsets.ConfigOptions, config flags type InputTypeScheme interface { runtime.Scheme[InputSpec, InputType] + Copy() InputTypeScheme + CreateConfigTypeSetConfigProvider() flagsets.ConfigTypeOptionSetConfigProvider GetInputType(name string) InputType @@ -179,6 +181,11 @@ func NewInputTypeScheme(defaultRepoDecoder runtime.TypedObjectDecoder[InputSpec] return &inputTypeScheme{scheme} } +func (t *inputTypeScheme) Copy() InputTypeScheme { + scheme := runtime.CopyScheme(t.Scheme) + return &inputTypeScheme{scheme} +} + func (t *inputTypeScheme) CreateConfigTypeSetConfigProvider() flagsets.ConfigTypeOptionSetConfigProvider { prov := flagsets.NewTypedConfigProvider("input", "blob input specification", "inputType") prov.AddGroups("Input Specification Options") 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/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..0149c2ee35 --- /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 in temp file + --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..2eb8ffdc6f --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/spec.go @@ -0,0 +1,97 @@ +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 + } + return acc.GetBlob() +} + +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() (blobaccess.BlobAccess, string, error) { + spec, err := json.Marshal(m.spec) + if err != nil { + return nil, "", errors.Wrapf(err, "cannot marshal access spec") + } + return accessobj.CachedBlobAccessForWriter(m.ctx, m.MimeType(), plugin.NewInputDataWriter(m.handler.plug, 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..84a6cb0753 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/inputs/types/plugin/testdata/plugin/inputhandlers/demo.go @@ -0,0 +1,105 @@ +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/extensions/accessmethods/options" + "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" +) + +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 PathOption = options.NewStringOptionType("inputPath", "path in temp file") + +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() []options.OptionType { + return []options.OptionType{ + options.MediatypeOption, + 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, 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, PathOption, config, "path")) + list.Add(flagsets.AddFieldByOptionP(opts, options.MediatypeOption, config, "mediaType")) + return list.Result() +} + +func (a *InputType) Reader(p ppi.Plugin, 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..3ab9d1cfd9 --- /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/extensions/accessmethods/options" + "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" +) + +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) +}