diff --git a/cmds/demoplugin/accessmethods/demo.go b/cmds/demoplugin/accessmethods/demo.go index 7b3e8f60a..858f9fc43 100644 --- a/cmds/demoplugin/accessmethods/demo.go +++ b/cmds/demoplugin/accessmethods/demo.go @@ -11,8 +11,9 @@ import ( "strings" "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/open-component-model/ocm/cmds/demoplugin/common" - "github.com/open-component-model/ocm/cmds/common" + "github.com/open-component-model/ocm/cmds/demoplugin/config" "github.com/open-component-model/ocm/pkg/cobrautils/flagsets" "github.com/open-component-model/ocm/pkg/contexts/credentials" "github.com/open-component-model/ocm/pkg/contexts/oci/identity" @@ -105,5 +106,15 @@ func (a *AccessMethod) ComposeAccessSpecification(p ppi.Plugin, opts ppi.Config, func (a *AccessMethod) Reader(p ppi.Plugin, spec ppi.AccessSpec, creds credentials.Credentials) (io.ReadCloser, error) { my := spec.(*AccessSpec) - return os.Open(filepath.Join(os.TempDir(), my.Path)) + cfg, _ := p.GetConfig() + root := os.TempDir() + if cfg != nil && cfg.(*config.Config).AccessMethods.Path != "" { + root = cfg.(*config.Config).Uploaders.Path + err := os.MkdirAll(root, 0o700) + if err != nil { + return nil, errors.Wrapf(err, "cannot create root dir") + } + } + + return os.Open(filepath.Join(root, my.Path)) } diff --git a/cmds/common/const.go b/cmds/demoplugin/common/const.go similarity index 100% rename from cmds/common/const.go rename to cmds/demoplugin/common/const.go diff --git a/cmds/demoplugin/config/config.go b/cmds/demoplugin/config/config.go new file mode 100644 index 000000000..87461e788 --- /dev/null +++ b/cmds/demoplugin/config/config.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "encoding/json" +) + +type Config struct { + AccessMethods Values `json:"accessMethods"` + Uploaders Values `json:"uploaders"` +} + +type Values struct { + Path string `json:"path"` +} + +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/demoplugin/main.go b/cmds/demoplugin/main.go index 2a8f53dc3..3d2a43607 100644 --- a/cmds/demoplugin/main.go +++ b/cmds/demoplugin/main.go @@ -8,6 +8,7 @@ import ( "os" "github.com/open-component-model/ocm/cmds/demoplugin/accessmethods" + "github.com/open-component-model/ocm/cmds/demoplugin/config" "github.com/open-component-model/ocm/cmds/demoplugin/uploaders" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds" @@ -19,6 +20,7 @@ func main() { p.SetShort("demo plugin") p.SetLong("plugin providing access to temp files.") + p.SetConfigParser(config.GetConfig) p.RegisterAccessMethod(accessmethods.New()) u := uploaders.New() diff --git a/cmds/demoplugin/uploaders/demo.go b/cmds/demoplugin/uploaders/demo.go index 6e013c2f5..ec48d4f8a 100644 --- a/cmds/demoplugin/uploaders/demo.go +++ b/cmds/demoplugin/uploaders/demo.go @@ -11,11 +11,13 @@ import ( "path/filepath" "strings" - "github.com/open-component-model/ocm/cmds/common" "github.com/open-component-model/ocm/cmds/demoplugin/accessmethods" + "github.com/open-component-model/ocm/cmds/demoplugin/common" + "github.com/open-component-model/ocm/cmds/demoplugin/config" "github.com/open-component-model/ocm/pkg/contexts/credentials" "github.com/open-component-model/ocm/pkg/contexts/oci/identity" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" + "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/runtime" ) @@ -76,10 +78,18 @@ func (a *Uploader) Writer(p ppi.Plugin, arttype, mediatype, hint string, repo pp var file *os.File var err error - my := repo.(*TargetSpec) + cfg, _ := p.GetConfig() + root := os.TempDir() + if cfg != nil && cfg.(*config.Config).Uploaders.Path != "" { + root = cfg.(*config.Config).Uploaders.Path + err := os.MkdirAll(root, 0o700) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot create root dir") + } + } path := hint - root := os.TempDir() + my := repo.(*TargetSpec) dir := root if my.Path != "" { root = filepath.Join(root, my.Path) diff --git a/cmds/helminstaller/app/execute.go b/cmds/helminstaller/app/execute.go index 854f916e3..2564e7796 100644 --- a/cmds/helminstaller/app/execute.go +++ b/cmds/helminstaller/app/execute.go @@ -64,7 +64,7 @@ func Execute(d driver.Driver, action string, ctx ocm.Context, octx out.Context, file.Close() os.Remove(path) - _, path, err = download.For(ctx).Download(octx, acc, path, osfs.New()) + _, path, err = download.For(ctx).Download(common.NewPrinter(octx.StdOut()), acc, path, osfs.New()) if err != nil { return errors.Wrapf(err, "downloading helm chart") } diff --git a/cmds/ocm/app/app.go b/cmds/ocm/app/app.go index 04ab9c9a0..540ba1990 100644 --- a/cmds/ocm/app/app.go +++ b/cmds/ocm/app/app.go @@ -39,6 +39,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/describe" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/download" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/get" + "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/install" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/show" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/sign" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/transfer" @@ -47,6 +48,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/topics/common/attributes" topicconfig "github.com/open-component-model/ocm/cmds/ocm/topics/common/config" topicocirefs "github.com/open-component-model/ocm/cmds/ocm/topics/oci/refs" + topicocmaccessmethods "github.com/open-component-model/ocm/cmds/ocm/topics/ocm/accessmethods" topicocmrefs "github.com/open-component-model/ocm/cmds/ocm/topics/ocm/refs" topicbootstrap "github.com/open-component-model/ocm/cmds/ocm/topics/toi/bootstrapping" "github.com/open-component-model/ocm/pkg/cobrautils" @@ -56,7 +58,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/datacontext" "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/vfsattr" datacfg "github.com/open-component-model/ocm/pkg/contexts/datacontext/config/attrs" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/utils" "github.com/open-component-model/ocm/pkg/errors" ocmlog "github.com/open-component-model/ocm/pkg/logging" @@ -191,6 +193,7 @@ func newCliCommand(opts *CLIOptions, mod ...func(clictx.Context, *cobra.Command) cmd.AddCommand(download.NewCommand(opts.Context)) cmd.AddCommand(bootstrap.NewCommand(opts.Context)) cmd.AddCommand(clean.NewCommand(opts.Context)) + cmd.AddCommand(install.NewCommand(opts.Context)) cmd.AddCommand(cmdutils.HideCommand(componentarchive.NewCommand(opts.Context))) cmd.AddCommand(cmdutils.HideCommand(resources.NewCommand(opts.Context))) @@ -220,12 +223,14 @@ func newCliCommand(opts *CLIOptions, mod ...func(clictx.Context, *cobra.Command) cmd.AddCommand(topicconfig.New(ctx)) cmd.AddCommand(topicocirefs.New(ctx)) cmd.AddCommand(topicocmrefs.New(ctx)) + cmd.AddCommand(topicocmaccessmethods.New(ctx)) cmd.AddCommand(attributes.New(ctx)) cmd.AddCommand(topicbootstrap.New(ctx, "toi-bootstrapping")) help.AddCommand(topicconfig.New(ctx)) help.AddCommand(topicocirefs.New(ctx)) help.AddCommand(topicocmrefs.New(ctx)) + help.AddCommand(topicocmaccessmethods.New(ctx)) help.AddCommand(topicbootstrap.New(ctx, "toi-bootstrapping")) for _, m := range mod { @@ -347,7 +352,7 @@ func (o *CLIOptions) Complete() error { } _ = ctx.ApplyConfig(spec, "cli") } - return plugincacheattr.Get(o.Context.OCMContext()).RegisterExtensions() + return registration.RegisterExtensions(o.Context.OCMContext()) } func NewVersionCommand(ctx clictx.Context) *cobra.Command { diff --git a/cmds/ocm/commands/ocmcmds/cmd.go b/cmds/ocm/commands/ocmcmds/cmd.go index e406e1feb..db0dcf405 100644 --- a/cmds/ocm/commands/ocmcmds/cmd.go +++ b/cmds/ocm/commands/ocmcmds/cmd.go @@ -18,6 +18,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/sources" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/versions" "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + topicocmaccessmethods "github.com/open-component-model/ocm/cmds/ocm/topics/ocm/accessmethods" topicocmrefs "github.com/open-component-model/ocm/cmds/ocm/topics/ocm/refs" "github.com/open-component-model/ocm/pkg/contexts/clictx" ) @@ -39,5 +40,7 @@ func NewCommand(ctx clictx.Context) *cobra.Command { cmd.AddCommand(plugins.NewCommand(ctx)) cmd.AddCommand(topicocmrefs.New(ctx)) + cmd.AddCommand(topicocmaccessmethods.New(ctx)) + return cmd } diff --git a/cmds/ocm/commands/ocmcmds/common/handlers/pluginhdlr/typehandler.go b/cmds/ocm/commands/ocmcmds/common/handlers/pluginhdlr/typehandler.go index 8f2de26d8..ef08804b3 100644 --- a/cmds/ocm/commands/ocmcmds/common/handlers/pluginhdlr/typehandler.go +++ b/cmds/ocm/commands/ocmcmds/common/handlers/pluginhdlr/typehandler.go @@ -5,11 +5,14 @@ package pluginhdlr import ( + "strings" + "github.com/open-component-model/ocm/cmds/ocm/pkg/output" "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" "github.com/open-component-model/ocm/pkg/contexts/clictx" "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" "github.com/open-component-model/ocm/pkg/errors" ) @@ -65,7 +68,22 @@ func (h *TypeHandler) Get(elemspec utils.ElemSpec) ([]output.Object, error) { p := cache.Get(elemspec.String()) if p == nil { - return nil, errors.ErrNotFound(ppi.KIND_PLUGIN, elemspec.String()) + objs := Lookup(elemspec.String(), cache) + if len(objs) == 0 { + return nil, errors.ErrNotFound(ppi.KIND_PLUGIN, elemspec.String()) + } + return objs, nil + } + return []output.Object{&Object{p}}, nil +} + +func Lookup(prefix string, cache plugins.Set) []output.Object { + var objs []output.Object + prefix = prefix + "." + for _, n := range cache.PluginNames() { + if strings.HasPrefix(n, prefix) { + objs = append(objs, &Object{cache.Get(n)}) + } } - return []output.Object{p}, nil + return objs } diff --git a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option.go b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option.go new file mode 100644 index 000000000..4f87c8910 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option.go @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package uploaderoption + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/pflag" + "sigs.k8s.io/yaml" + + "github.com/open-component-model/ocm/cmds/ocm/pkg/options" + "github.com/open-component-model/ocm/pkg/cobrautils/flag" + "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" + "github.com/open-component-model/ocm/pkg/errors" +) + +func From(o options.OptionSetProvider) *Option { + var opt *Option + o.AsOptionSet().Get(&opt) + return opt +} + +type Registration struct { + Name string + ArtefactType string + MediaType string + Config json.RawMessage +} + +func New() *Option { + return &Option{} +} + +type Option struct { + spec map[string]string + Registrations []*Registration +} + +func (o *Option) AddFlags(fs *pflag.FlagSet) { + flag.StringToStringVarP(fs, &o.spec, "uploader", "", nil, "repository uploader (::== 0 { + art = nam[i+1:] + nam = nam[:i] + i = strings.Index(art, ":") + if i >= 0 { + med = art[i+1:] + art = art[:i] + i = strings.Index(med, ":") + if i >= 0 { + return fmt.Errorf("invalid uploader registration %s must be of %s", n, desc) + } + } + } + + var data json.RawMessage + var err error + if strings.HasPrefix(v, "@") { + data, err = vfs.ReadFile(ctx.FileSystem(), v[1:]) + if err != nil { + return errors.Wrapf(err, "cannot read upload target specification from %q", v[1:]) + } + } else { + var values interface{} + if err = yaml.Unmarshal([]byte(v), &values); err != nil { + return errors.Wrapf(err, "invalid target specification %q", v) + } + data, err = json.Marshal(values) + if err != nil { + return errors.Wrapf(err, "cannot marshal target specification") + } + } + o.Registrations = append(o.Registrations, &Registration{ + Name: nam, + ArtefactType: art, + MediaType: med, + Config: data, + }) + } + return nil +} + +func (o *Option) Register(ctx ocm.ContextProvider) error { + for _, s := range o.Registrations { + err := registration.RegisterBlobHandlerByName(ctx.OCMContext(), s.Name, s.Config, + registration.ForArtefactType(s.ArtefactType), registration.ForMimeType(s.MediaType)) + if err != nil { + return err + } + } + return nil +} + +func (o *Option) Usage() string { + s := ` +If the --uploader option is specified, appropriate uploaders +are configured for the transport target. It has the following format + +
+
<name>:<artefact type>:<media type>=<yaml target config>
+
+ +The uploader name may be a path expression with the following possibilities: +- ocm/ociRegistry: oci Registry upload for local OCI artefact blobs. + The media type is optional. If given ist must be an OCI artefact media type. +- plugin/[/: uploader provided by plugin. +` + return s +} diff --git a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/suite_test.go b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/suite_test.go new file mode 100644 index 000000000..e89834e31 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package uploaderoption + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Uploader Option Suite") +} diff --git a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/uploader_test.go b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/uploader_test.go new file mode 100644 index 000000000..5b033889a --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/uploader_test.go @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package uploaderoption + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/testutils" + + "github.com/spf13/pflag" + + "github.com/open-component-model/ocm/pkg/contexts/clictx" +) + +var _ = Describe("uploader option", func() { + var flags *pflag.FlagSet + var opt *Option + var ctx clictx.Context + + BeforeEach(func() { + ctx = clictx.New() + flags = pflag.NewFlagSet("test", pflag.ContinueOnError) + opt = &Option{} + opt.AddFlags(flags) + }) + + It("parsed n:a:m", func() { + MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name:art:media={"name":"Name"}`})) + MustBeSuccessful(opt.Complete(ctx)) + + Expect(opt.Registrations).To(Equal([]*Registration{&Registration{ + Name: "plugin/name", + ArtefactType: "art", + MediaType: "media", + Config: json.RawMessage(`{"name":"Name"}`), + }})) + }) + + It("parsed n:a", func() { + MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name:art={"name":"Name"}`})) + MustBeSuccessful(opt.Complete(ctx)) + + Expect(opt.Registrations).To(Equal([]*Registration{&Registration{ + Name: "plugin/name", + ArtefactType: "art", + MediaType: "", + Config: json.RawMessage(`{"name":"Name"}`), + }})) + }) + + It("parsed n", func() { + MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name={"name":"Name"}`})) + MustBeSuccessful(opt.Complete(ctx)) + + Expect(opt.Registrations).To(Equal([]*Registration{&Registration{ + Name: "plugin/name", + ArtefactType: "", + MediaType: "", + Config: json.RawMessage(`{"name":"Name"}`), + }})) + }) + + It("parsed n::", func() { + MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name::={"name":"Name"}`})) + MustBeSuccessful(opt.Complete(ctx)) + + Expect(opt.Registrations).To(Equal([]*Registration{&Registration{ + Name: "plugin/name", + ArtefactType: "", + MediaType: "", + Config: json.RawMessage(`{"name":"Name"}`), + }})) + }) + + It("parsed flat spec", func() { + MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name=Name`})) + MustBeSuccessful(opt.Complete(ctx)) + + Expect(opt.Registrations).To(Equal([]*Registration{&Registration{ + Name: "plugin/name", + ArtefactType: "", + MediaType: "", + Config: json.RawMessage(`"Name"`), + }})) + }) + + It("fails", func() { + MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name:::=Name`})) + MustFailWithMessage(opt.Complete(ctx), "invalid uploader registration plugin/name::: must be of [:[:]]") + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/componentarchive/create/cmd.go b/cmds/ocm/commands/ocmcmds/componentarchive/create/cmd.go index 9e2167561..b3afe2d0d 100644 --- a/cmds/ocm/commands/ocmcmds/componentarchive/create/cmd.go +++ b/cmds/ocm/commands/ocmcmds/componentarchive/create/cmd.go @@ -128,6 +128,7 @@ func (o *Command) Run() error { } obj, err := comparch.Create(o.Context.OCMContext(), accessobj.ACC_CREATE, o.Path, mode, o.Handler, fs) if err != nil { + fs.RemoveAll(o.Path) return err } desc := obj.GetDescriptor() @@ -141,7 +142,12 @@ func (o *Command) Run() error { err = compdesc.Validate(desc) if err != nil { obj.Close() + fs.RemoveAll(o.Path) return errors.Newf("invalid component info: %s", err) } - return obj.Close() + err = obj.Close() + if err != nil { + fs.RemoveAll(o.Path) + } + return err } diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go b/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go index 294b8871c..55c1174b9 100644 --- a/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go +++ b/cmds/ocm/commands/ocmcmds/components/transfer/cmd.go @@ -18,6 +18,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/repooption" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/rscbyvalueoption" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/scriptoption" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/uploaderoption" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/names" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" "github.com/open-component-model/ocm/cmds/ocm/pkg/output" @@ -52,6 +53,7 @@ func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { lookupoption.New(), overwriteoption.New(), rscbyvalueoption.New(), + uploaderoption.New(), scriptoption.New(), )}, utils.Names(Names, names...)...) } @@ -90,6 +92,12 @@ func (o *Command) Run() error { if err != nil { return err } + + err = uploaderoption.From(o).Register(o.Context) + if err != nil { + return err + } + target, err := ocm.AssureTargetRepository(session, o.Context.OCMContext(), o.TargetName, ocm.CommonTransportFormat, formatoption.From(o).Format, o.Context.FileSystem()) if err != nil { return err diff --git a/cmds/ocm/commands/ocmcmds/components/transfer/upload_test.go b/cmds/ocm/commands/ocmcmds/components/transfer/upload_test.go new file mode 100644 index 000000000..bdfac5228 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/transfer/upload_test.go @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package transfer_test + +import ( + "bytes" + "encoding/json" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/cmds/ocm/testhelper" + . "github.com/open-component-model/ocm/pkg/contexts/oci/testhelper" + . "github.com/open-component-model/ocm/pkg/testutils" + + "github.com/open-component-model/ocm/pkg/common/accessio" + "github.com/open-component-model/ocm/pkg/common/accessobj" + "github.com/open-component-model/ocm/pkg/contexts/oci" + "github.com/open-component-model/ocm/pkg/contexts/oci/grammar" + ctfoci "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ctf" + "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/ociartefact" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/ociuploadattr" + v1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/comparch" + ctfocm "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" + "github.com/open-component-model/ocm/pkg/contexts/ocm/resourcetypes" + "github.com/open-component-model/ocm/pkg/contexts/ocm/transfer" + "github.com/open-component-model/ocm/pkg/contexts/ocm/transfer/transferhandler/standard" + "github.com/open-component-model/ocm/pkg/mime" +) + +const COMP = "github.com/compa" +const VERS = "1.0.0" +const CA = "ca" +const CTF = "ctf" +const COPY = "./ctf.copy" +const TARGET = "/tmp/target" + +const OCISRC = "/tmp/source" + +var _ = Describe("upload", func() { + var env *TestEnv + + BeforeEach(func() { + env = NewTestEnv() + + // fake OCI registry + spec := Must(ctfoci.NewRepositorySpec(accessobj.ACC_READONLY, OCISRC, accessio.PathFileSystem(env.FileSystem()))) + env.OCIContext().SetAlias(OCIHOST, spec) + + env.OCICommonTransport(OCISRC, accessio.FormatDirectory, func() { + env.Namespace(OCINAMESPACE, func() { + env.Manifest(OCIVERSION, func() { + env.Config(func() { + env.BlobStringData(mime.MIME_JSON, "{}") + }) + env.Layer(func() { + env.BlobStringData(mime.MIME_TEXT, "manifestlayer") + }) + }) + }) + }) + + env.OCICommonTransport(TARGET, accessio.FormatDirectory) + + env.ComponentArchive(CA, accessio.FormatDirectory, COMP, VERS, func() { + env.Provider("mandelsoft") + env.Resource("value", "", resourcetypes.OCI_IMAGE, v1.LocalRelation, func() { + env.Access( + ociartefact.New(oci.StandardOCIRef(OCIHOST+".alias", OCINAMESPACE, OCIVERSION)), + ) + }) + }) + + ca := Must(comparch.Open(env.OCMContext(), accessobj.ACC_READONLY, CA, 0, env)) + oca := accessio.OnceCloser(ca) + defer Close(oca) + + ctf := Must(ctfocm.Create(env.OCMContext(), accessobj.ACC_CREATE, CTF, 0700, env)) + octf := accessio.OnceCloser(ctf) + defer Close(octf) + + handler := Must(standard.New(standard.ResourcesByValue())) + + MustBeSuccessful(transfer.TransferVersion(nil, nil, ca, ctf, handler)) + + // now we have a transport archive with local blob for the image + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("transfers oci artefact with named handler", func() { + ctx := env.OCMContext() + config := Must(json.Marshal(ociuploadattr.New(TARGET + grammar.RepositorySeparator + grammar.RepositorySeparator + "copy"))) + + buf := bytes.NewBuffer(nil) + err := env.CatchOutput(buf).Execute("transfer", "components", "--uploader", "ocm/ociArtifacts="+string(config), "--copy-resources", CTF, COPY) + if err != nil { + fmt.Printf("%s\n", buf.String()) + Expect(err).To(Succeed()) + } + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +transferring version "github.com/compa:1.0.0"... +...resource 0(ocm/value:v2.0)... +...adding component version... +1 versions transferred + +`)) + + copy := Must(ctfocm.Open(ctx, accessobj.ACC_READONLY, COPY, 0700, env)) + ocopy := accessio.OnceCloser(copy) + defer Close(ocopy) + + // check type + cv2 := Must(copy.LookupComponentVersion(COMP, VERS)) + ocv2 := accessio.OnceCloser(cv2) + defer Close(ocv2) + ra := Must(cv2.GetResourceByIndex(0)) + acc := Must(ra.Access()) + Expect(acc.GetKind()).To(Equal(ociartefact.Type)) + val := Must(ctx.AccessSpecForSpec(acc)) + // TODO: the result is invalid for ctf: better handling for ctf refs + Expect(val.(*ociartefact.AccessSpec).ImageReference).To(Equal("/tmp/target//copy/ocm/value:v2.0")) + + target, err := ctfoci.Open(ctx.OCIContext(), accessobj.ACC_READONLY, TARGET, 0, env) + Expect(err).To(Succeed()) + defer Close(target) + Expect(target.ExistsArtefact("copy/ocm/value", "v2.0")).To(BeTrue()) + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go b/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go index 06c108f84..2b74ce502 100644 --- a/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go +++ b/cmds/ocm/commands/ocmcmds/ctf/transfer/cmd.go @@ -12,6 +12,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/overwriteoption" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/rscbyvalueoption" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/scriptoption" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/uploaderoption" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/names" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" @@ -44,6 +45,7 @@ func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { formatoption.New(), overwriteoption.New(), rscbyvalueoption.New(), + uploaderoption.New(), scriptoption.New(), )}, utils.Names(Names, names...)...) } @@ -77,6 +79,11 @@ func (o *Command) Run() error { return err } + err = uploaderoption.From(o).Register(o.Context) + if err != nil { + return err + } + src, err := ctf.Open(o.Context.OCMContext(), accessobj.ACC_READONLY, o.SourceName, 0, o.FileSystem()) if err != nil { return errors.Wrapf(err, "cannot open source") diff --git a/cmds/ocm/commands/ocmcmds/names/names.go b/cmds/ocm/commands/ocmcmds/names/names.go index e9c155505..b9ccbaa97 100644 --- a/cmds/ocm/commands/ocmcmds/names/names.go +++ b/cmds/ocm/commands/ocmcmds/names/names.go @@ -14,5 +14,5 @@ var ( Sources = []string{"sources", "source", "src", "s"} References = []string{"references", "reference", "refs"} Versions = []string{"versions", "vers", "v"} - Plugins = []string{"plugins", "p"} + Plugins = []string{"plugins", "plugin", "p"} ) diff --git a/cmds/ocm/commands/ocmcmds/plugins/cmd.go b/cmds/ocm/commands/ocmcmds/plugins/cmd.go index 26abc813c..9333d7276 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/cmd.go +++ b/cmds/ocm/commands/ocmcmds/plugins/cmd.go @@ -8,7 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/names" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/plugins/describe" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/plugins/get" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/plugins/install" "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" "github.com/open-component-model/ocm/pkg/contexts/clictx" ) @@ -21,5 +23,7 @@ func NewCommand(ctx clictx.Context) *cobra.Command { Short: "Commands related to OCM plugins", }, Names...) cmd.AddCommand(get.NewCommand(ctx, get.Verb)) + cmd.AddCommand(install.NewCommand(ctx, install.Verb)) + cmd.AddCommand(describe.NewCommand(ctx, describe.Verb)) return cmd } diff --git a/cmds/ocm/commands/ocmcmds/plugins/common/common.go b/cmds/ocm/commands/ocmcmds/plugins/common/common.go new file mode 100644 index 000000000..efbf1d356 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/common/common.go @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package common + +import ( + "fmt" + "strings" +) + +type Stringer interface { + String() string +} + +type Element[C Stringer] interface { + GetName() string + GetConstraints() []C +} + +func DescribeElements[E Element[C], C Stringer](elems []E) string { + var list []string + for _, m := range elems { + n := m.GetName() + var clist []string + for _, c := range m.GetConstraints() { + clist = append(clist, c.String()) + } + if len(clist) > 0 { + n = fmt.Sprintf("%s[%s]", n, strings.Join(clist, ",")) + } + list = append(list, n) + } + return strings.Join(list, ",") +} diff --git a/cmds/ocm/commands/ocmcmds/plugins/common/describe.go b/cmds/ocm/commands/ocmcmds/plugins/common/describe.go new file mode 100644 index 000000000..300c82bdd --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/common/describe.go @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package common + +import ( + "encoding/json" + "strings" + + "github.com/open-component-model/ocm/pkg/common" + "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/options" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" + utils2 "github.com/open-component-model/ocm/pkg/utils" +) + +func DescribePlugin(p plugin.Plugin, out common.Printer) { + out.Printf("Plugin Name: %s\n", p.Name()) + out.Printf("Plugin Version: %s\n", p.Version()) + out.Printf("Path: %s\n", p.Path()) + + if !p.IsValid() { + out.Printf("Status: %s\n", p.Error()) + return + } + out.Printf("Status: %s\n", "valid") + var caps []string + if len(p.GetDescriptor().AccessMethods) > 0 { + caps = append(caps, "Access Methods") + } + if len(p.GetDescriptor().Uploaders) > 0 { + caps = append(caps, "Repository Uploaders") + } + if len(p.GetDescriptor().Downloaders) > 0 { + caps = append(caps, "Resource Downloaders") + } + if len(caps) == 0 { + out.Printf("Capabilities: none\n") + } else { + out.Printf("Capabilities: %s\n", strings.Join(caps, ", ")) + } + src := p.GetSource() + if src != nil { + out.Printf("Source:\n") + out.Printf(" Component: %s\n", src.Component) + out.Printf(" Version: %s\n", src.Version) + out.Printf(" Resource: %s\n", src.Resource) + u := src.Repository.AsUniformSpec(p.Context()) + data, _ := json.Marshal(src.Repository) + out.Printf(" Repository: %s\n", u.String()) + out.Printf(" Specification: %s\n", string(data)) + } else { + out.Printf("Source: manually installed\n") + } + out.Printf("\n") + out.Printf("Description: \n") + if p.GetDescriptor().Long == "" { + out.Printf("%s\n", utils2.IndentLines(p.GetDescriptor().Short, " ")) + } else { + out.Printf("%s\n", utils2.IndentLines(p.GetDescriptor().Long, " ")) + } + if len(p.GetDescriptor().AccessMethods) > 0 { + out.Printf("\n") + out.Printf("Access Methods:\n") + DescribeAccessMethods(p, out) + } + if len(p.GetDescriptor().Uploaders) > 0 { + out.Printf("\n") + // a working type inference would be really great + ListElements[plugin.UploaderDescriptor, plugin.UploaderKey]("Repository Uploaders", p.GetDescriptor().Uploaders, out) + } + if len(p.GetDescriptor().Downloaders) > 0 { + out.Printf("\n") + ListElements[plugin.DownloaderDescriptor, plugin.DownloaderKey]("Resource Downloaders", p.GetDescriptor().Downloaders, out) + } +} + +type MethodInfo struct { + Name string + Description string + Versions map[string]*MethodVersion +} + +type MethodVersion struct { + Name string + Format string + Options map[string]options.OptionType +} + +func GetAccessMethodInfo(methods []plugin.AccessMethodDescriptor) map[string]*MethodInfo { + found := map[string]*MethodInfo{} + for _, m := range methods { + i := found[m.Name] + if i == nil { + i = &MethodInfo{ + Name: m.Name, + Description: m.Description, + Versions: map[string]*MethodVersion{}, + } + found[m.Name] = i + } + if i.Description == "" { + i.Description = m.Description + } + vers := m.Version + if m.Version == "" { + vers = "v1" + } + v := i.Versions[vers] + if v == nil { + v = &MethodVersion{ + Name: vers, + Options: map[string]options.OptionType{}, + } + i.Versions[vers] = v + } + if v.Format == "" { + v.Format = m.Format + } + if (len(v.Options) == 0 || m.Version != "") && len(m.CLIOptions) > 0 { + for _, o := range m.CLIOptions { + if o.Name == "" { + continue + } + opt := options.DefaultRegistry.GetOptionType(o.Name) + if opt == nil { + t, err := options.DefaultRegistry.CreateOptionType(o.Type, o.Name, o.Description) + if err != nil { + continue + } + opt = t + } + v.Options[opt.GetName()] = opt + } + } + } + return found +} + +func DescribeAccessMethods(p plugin.Plugin, out common.Printer) { + d := p.GetDescriptor() + + methods := GetAccessMethodInfo(d.AccessMethods) + + for _, n := range utils2.StringMapKeys(methods) { + out.Printf("- Name: %s\n", n) + m := methods[n] + if m.Description != "" { + out.Printf("%s\n", utils2.IndentLines(m.Description, " ")) + } + out := out.AddGap(" ") + out.Printf("Versions:\n") + for _, vn := range utils2.StringMapKeys(m.Versions) { + out.Printf("- Version: %s\n", vn) + out := out.AddGap(" ") + v := m.Versions[vn] + if v.Format != "" { + out.Printf("%s\n", v.Format) + } + if len(v.Options) > 0 { + out.Printf("Command Line Options:") + out.Printf("%s\n", utils2.FormatMap("", v.Options)) + } + } + } +} + +type Describable interface { + Describe() string +} + +type DescribableElement[C Describable] interface { + GetName() string + GetDescription() string + GetConstraints() []C +} + +func ListElements[E DescribableElement[C], C Describable](msg string, elems []E, out common.Printer) { + var list []string + + keys := map[string]E{} + for _, e := range elems { + keys[e.GetName()] = e + } + if len(keys) > 0 { + out.Printf("%s:\n", msg) + } + for _, n := range utils2.StringMapKeys(keys) { + m := keys[n] + out.Printf("- Name: %s\n", n) + if m.GetDescription() != "" { + desc := m.GetDescription() + if !strings.HasSuffix(desc, "\n") { + desc += "\n" + } + out.AddGap(" ").Printf("%s\n", desc) + } + if len(m.GetConstraints()) > 0 { + out := out.AddGap(" ") + out.Printf("Registration Contraints:\n") + for _, c := range m.GetConstraints() { + out.Printf("- %s\n", utils2.IndentLines(c.Describe(), " ", true)) + } + } + list = append(list, n) + } +} diff --git a/cmds/ocm/commands/ocmcmds/plugins/describe/cmd.go b/cmds/ocm/commands/ocmcmds/plugins/describe/cmd.go new file mode 100644 index 000000000..671f3b1d3 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/describe/cmd.go @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package describe + +import ( + "github.com/spf13/cobra" + + handler "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/handlers/pluginhdlr" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/names" + common2 "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/plugins/common" + "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" + "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + "github.com/open-component-model/ocm/pkg/cobrautils" + "github.com/open-component-model/ocm/pkg/common" + "github.com/open-component-model/ocm/pkg/contexts/clictx" +) + +var ( + Names = names.Plugins + Verb = verbs.Describe +) + +type Command struct { + utils.BaseCommand + + Names []string +} + +// NewCommand creates a new ctf command. +func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { + return utils.SetupCommand( + &Command{ + BaseCommand: utils.NewBaseCommand(ctx), + }, + utils.Names(Names, names...)..., + ) +} + +func (o *Command) ForName(name string) *cobra.Command { + return &cobra.Command{ + Use: "[] {}", + Short: "get plugins", + Long: ` +Describes provides comprehensive information about the capabilities of +a plugin. +`, + Example: ` +$ ocm describe plugins +$ ocm describe plugins demo +`, + } +} + +func (o *Command) Complete(args []string) error { + o.Names = args + return nil +} + +func (o *Command) Run() error { + hdlr := handler.NewTypeHandler(o.Context.OCM()) + return utils.HandleOutput(NewAction(o), hdlr, utils.StringElemSpecs(o.Names...)...) +} + +///////////////////////////////////////////////////////////////////////////// + +type action struct { + Printer common.Printer + Count int +} + +func NewAction(o *Command) *action { + return &action{ + Printer: common.NewPrinter(o.StdOut()), + } +} + +func (a *action) Add(e interface{}) error { + a.Count++ + p := handler.Elem(e) + + out, buf := common.NewBufferedPrinter() + common2.DescribePlugin(p, out) + if a.Count > 1 { + a.Printer.Printf("----------------------\n") + } + a.Printer.Printf("%s\n", cobrautils.CleanMarkdown(buf.String())) + return nil +} + +func (a *action) Close() error { + return nil +} + +func (a *action) Out() error { + a.Printer.Printf("*** found %d plugins\n", a.Count) + return nil +} diff --git a/cmds/ocm/commands/ocmcmds/plugins/describe/cmd_test.go b/cmds/ocm/commands/ocmcmds/plugins/describe/cmd_test.go new file mode 100644 index 000000000..872c7468e --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/describe/cmd_test.go @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build unix + +package describe_test + +import ( + "bytes" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/cmds/ocm/testhelper" + . "github.com/open-component-model/ocm/pkg/testutils" +) + +const PLUGINS = "/testdata" + +var _ = Describe("Test Environment", func() { + var env *TestEnv + var path string + + BeforeEach(func() { + env = NewTestEnv(TestData()) + + p, err := filepath.Abs("testdata") + Expect(err).To(Succeed()) + path = p + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("get plugins", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+path, "describe", "plugins")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +Plugin Name: test +Plugin Version: v1 +Path: ` + path + `/test +Status: valid +Capabilities: Access Methods +Source: manually installed +Description: + a test plugin with access method test +Access Methods: +- Name: test + Versions: + - Version: v1 +*** found 1 plugins +`)) + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/plugins/describe/suite_test.go b/cmds/ocm/commands/ocmcmds/plugins/describe/suite_test.go new file mode 100644 index 000000000..f2f1a448d --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/describe/suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package describe_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OCM describe plugins") +} diff --git a/cmds/ocm/commands/ocmcmds/plugins/describe/testdata/test b/cmds/ocm/commands/ocmcmds/plugins/describe/testdata/test new file mode 100755 index 000000000..265973086 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/describe/testdata/test @@ -0,0 +1,21 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +# +# SPDX-License-Identifier: Apache-2.0 + +NAME="$(basename "$0")" + +Error() { + echo '{ "error": "'$1'" }' >&2 + exit 1 +} + +Info() { + echo '{"version":"v1","pluginName":"'$NAME'","pluginVersion":"v1","shortDescription":"a test plugin without function","description":"a test plugin with access method test","accessMethods":[{"name":"test","shortDescription":"test access","description":""},{"name":"test","version":"v1","shortDescription":"test access","description":""}]}' +} + +case "$1" in + info) Info;; + *) Error "invalid command $1";; +esac diff --git a/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go b/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go index 6d9b22662..ef088a36a 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go +++ b/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go @@ -6,19 +6,21 @@ package get import ( "fmt" + "sort" "strings" "github.com/spf13/cobra" handler "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/handlers/pluginhdlr" - "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/repooption" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/names" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/plugins/common" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" "github.com/open-component-model/ocm/cmds/ocm/pkg/output" "github.com/open-component-model/ocm/cmds/ocm/pkg/processing" "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" "github.com/open-component-model/ocm/pkg/contexts/clictx" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" + utils2 "github.com/open-component-model/ocm/pkg/utils" ) var ( @@ -36,7 +38,7 @@ type Command struct { func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { return utils.SetupCommand( &Command{ - BaseCommand: utils.NewBaseCommand(ctx, repooption.New(), output.OutputOptions(outputs)), + BaseCommand: utils.NewBaseCommand(ctx, output.OutputOptions(outputs)), }, utils.Names(Names, names...)..., ) @@ -46,6 +48,10 @@ func (o *Command) ForName(name string) *cobra.Command { return &cobra.Command{ Use: "[] {}", Short: "get plugins", + Long: ` +Get lists information for all plugins specified, if no plugin is specified +all registered ones are listed. +`, Example: ` $ ocm get plugins $ ocm get plugins demo -o yaml @@ -53,13 +59,6 @@ $ ocm get plugins demo -o yaml } } -func (o *Command) Long() string { - return ` -Get lists information for all plugins specified, if no plugin is specified -all registered ones are listed. -` -} - func (o *Command) Complete(args []string) error { o.Names = args return nil @@ -74,7 +73,7 @@ func (o *Command) Run() error { func TableOutput(opts *output.Options, mapping processing.MappingFunction, wide ...string) *output.TableOutput { def := &output.TableOutput{ - Headers: output.Fields("PLUGIN", "VERSION", "DESCRIPTION", wide), + Headers: output.Fields("PLUGIN", "VERSION", "SOURCE", "DESCRIPTION", wide), Options: opts, Mapping: mapping, } @@ -97,50 +96,42 @@ func getWide(opts *output.Options) output.Output { func mapGetRegularOutput(e interface{}) interface{} { p := handler.Elem(e) - return []string{p.Name(), p.Version(), p.Message()} + loc := "local" + src := p.GetSource() + if src != nil { + loc = src.Component + ":" + src.Version + } + return []string{p.Name(), p.Version(), loc, p.Message()} } func mapGetWideOutput(e interface{}) interface{} { p := handler.Elem(e) d := p.GetDescriptor() - var list []string + found := map[string][]string{} for _, m := range d.AccessMethods { - n := m.Name - if m.Version != "" { - n += "/" + m.Version + l := found[m.Name] + v := m.Version + if v != "" { + l = append(l, v) } - list = append(list, n) + found[m.Name] = l } - // a working type inference would be really great - ups := Describe[plugin.UploaderDescriptor, plugin.UploaderKey](d.Uploaders) - downs := Describe[plugin.DownloaderDescriptor, plugin.DownloaderKey](d.Downloaders) - - return output.Fields(mapGetRegularOutput(e), strings.Join(list, ","), ups, downs) -} - -func Describe[E Element[C], C Stringer](elems []E) string { var list []string - for _, m := range elems { - n := m.GetName() - var clist []string - for _, c := range m.GetConstraints() { - clist = append(clist, c.String()) - } - if len(clist) > 0 { - n = fmt.Sprintf("%s[%s]", n, strings.Join(clist, ",")) + for _, m := range utils2.StringMapKeys(found) { + l := found[m] + if len(l) == 0 { + list = append(list, m) + } else { + sort.Strings(l) + list = append(list, fmt.Sprintf("%s[%s]", m, strings.Join(l, ","))) } - list = append(list, n) } - return strings.Join(list, ",") -} -type Stringer interface { - String() string -} + // a working type inference would be really great + ups := common.DescribeElements[plugin.UploaderDescriptor, plugin.UploaderKey](d.Uploaders) + downs := common.DescribeElements[plugin.DownloaderDescriptor, plugin.DownloaderKey](d.Downloaders) -type Element[C Stringer] interface { - GetName() string - GetConstraints() []C + return output.Fields(mapGetRegularOutput(e), strings.Join(list, ","), ups, downs) } diff --git a/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go b/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go index 9149ae5c7..bce4852fb 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go @@ -39,8 +39,8 @@ var _ = Describe("Test Environment", func() { Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+path, "get", "plugins")).To(Succeed()) Expect(buf.String()).To(StringEqualTrimmedWithContext( ` -PLUGIN VERSION DESCRIPTION -test v1 a test plugin without function +PLUGIN VERSION SOURCE DESCRIPTION +test v1 local a test plugin without function `)) }) It("get plugins with additional info", func() { @@ -48,8 +48,8 @@ test v1 a test plugin without function Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+path, "get", "plugins", "-o", "wide")).To(Succeed()) Expect(buf.String()).To(StringEqualTrimmedWithContext( ` -PLUGIN VERSION DESCRIPTION ACCESSMETHODS UPLOADERS DOWNLOADERS -test v1 a test plugin without function test,test/v1 +PLUGIN VERSION SOURCE DESCRIPTION ACCESSMETHODS UPLOADERS DOWNLOADERS +test v1 local a test plugin without function test[v1] `)) }) diff --git a/cmds/ocm/commands/ocmcmds/plugins/get/suite_test.go b/cmds/ocm/commands/ocmcmds/plugins/get/suite_test.go index 0986c50bf..1fc1b991d 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/get/suite_test.go +++ b/cmds/ocm/commands/ocmcmds/plugins/get/suite_test.go @@ -13,5 +13,5 @@ import ( func TestConfig(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "OCM get components") + RunSpecs(t, "OCM get plugins") } diff --git a/cmds/ocm/commands/ocmcmds/plugins/install/cmd.go b/cmds/ocm/commands/ocmcmds/plugins/install/cmd.go new file mode 100644 index 000000000..5ed274af8 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/install/cmd.go @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package install + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + ocmcommon "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/handlers/pluginhdlr" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/common/options/repooption" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/names" + "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" + "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + "github.com/open-component-model/ocm/pkg/cobrautils/flag" + "github.com/open-component-model/ocm/pkg/common" + "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" + "github.com/open-component-model/ocm/pkg/errors" + "github.com/open-component-model/ocm/pkg/out" +) + +var ( + Names = names.Plugins + Verb = verbs.Install +) + +type Command struct { + utils.BaseCommand + + cache.PluginUpdater + + Ref string + Names []string +} + +// NewCommand creates a new ctf command. +func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { + return utils.SetupCommand( + &Command{ + BaseCommand: utils.NewBaseCommand(ctx, repooption.New()), + }, + utils.Names(Names, names...)..., + ) +} + +func (o *Command) ForName(name string) *cobra.Command { + return &cobra.Command{ + Use: "[] [] | ", + Short: "install or update an OCM plugin", + Long: ` +Download and install a plugin provided by an OCM component version. +For the update mode only the plugin name is required. + +If no version is specified the latest version is chosen. If at least one +version constraint is given, only the matching versions are considered. +`, + Args: cobra.MinimumNArgs(1), + Example: ` +$ ocm install plugin ghcr.io/github.com/mandelsoft/cnudie//github.com/mandelsoft/ocmplugin:0.1.0-dev +$ ocm install plugin -c 1.2.x ghcr.io/github.com/mandelsoft/cnudie//github.com/mandelsoft/ocmplugin +$ ocm install plugin -u demo +$ ocm install plugin -r demo +`, + } +} +func (o *Command) AddFlags(fs *pflag.FlagSet) { + fs.BoolVarP(&o.RemoveMode, "remove", "r", false, "remove plugin") + fs.BoolVarP(&o.UpdateMode, "update", "u", false, "update plugin") + fs.BoolVarP(&o.Describe, "describe", "d", false, "describe plugin, only") + fs.BoolVarP(&o.Force, "force", "f", false, "overwrite existing plugin") + flag.SemverConstraintsVarP(fs, &o.Constraints, "constraints", "c", nil, "version constraint") +} + +func (o *Command) Complete(args []string) error { + o.PluginUpdater.Context = o.BaseCommand.OCMContext() + + if o.UpdateMode && o.RemoveMode { + return fmt.Errorf("either remove or update mode possible, only") + } + if o.UpdateMode || o.RemoveMode { + o.Names = args + if len(args) == 0 { + return fmt.Errorf("for update mode the plugin name is required") + } + } else { + if len(args) > 2 { + return fmt.Errorf("only two arguments ( []) possible") + } + if len(args) > 1 { + o.Names = []string{args[1]} + } + o.Ref = args[0] + } + o.Printer = common.NewPrinter(o.StdOut()) + return nil +} + +func (o *Command) Run() error { + session := ocm.NewSession(nil) + defer session.Close() + + err := o.ProcessOnOptions(ocmcommon.CompleteOptionsWithSession(o.BaseCommand.Context, session)) + if err != nil { + return err + } + + repo := repooption.From(o) + + if o.UpdateMode || o.RemoveMode { + msg := []string{"updating", "updated"} + f := o.Update + if o.RemoveMode { + msg = []string{"removing", "removed"} + f = o.Remove + } + pi := plugincacheattr.Get(o.OCMContext()) + failed := 0 + for _, n := range o.Names { + o.Ref = n + if pi.Get(n) == nil { + objs := pluginhdlr.Lookup(n, pi) + if len(objs) == 1 { + o.Ref = pluginhdlr.Elem(objs[0]).Name() + } + } + if err := f(session, o.Ref); err != nil { + if len(o.Names) == 1 { + return errors.Wrapf(err, "%s plugin %s failed", msg[0], o.Ref) + } + out.Errf(o, "%s plugin %s failed: %s\n", msg[0], o.Ref, err.Error()) + failed++ + } + } + if failed > 0 { + return fmt.Errorf("%s of %d plugin(s) failed", msg[0], failed) + } + return nil + } + name := "" + if len(o.Names) > 0 { + name = o.Names[0] + } + if repo.Repository != nil { + return o.DownloadFromRepo(session, repo.Repository, o.Ref, name) + } + return o.DownloadRef(session, o.Ref, name) +} + +///////////////////////////////////////////////////////////////////////////// diff --git a/cmds/ocm/commands/ocmcmds/plugins/install/suite_test.go b/cmds/ocm/commands/ocmcmds/plugins/install/suite_test.go new file mode 100644 index 000000000..714be3d1a --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/plugins/install/suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package install_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OCM get components") +} diff --git a/cmds/ocm/commands/ocmcmds/resources/download/cmd.go b/cmds/ocm/commands/ocmcmds/resources/download/cmd.go index 26c45a739..e3549d4df 100644 --- a/cmds/ocm/commands/ocmcmds/resources/download/cmd.go +++ b/cmds/ocm/commands/ocmcmds/resources/download/cmd.go @@ -24,6 +24,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" "github.com/open-component-model/ocm/cmds/ocm/pkg/output" "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + common2 "github.com/open-component-model/ocm/pkg/common" "github.com/open-component-model/ocm/pkg/contexts/clictx" "github.com/open-component-model/ocm/pkg/contexts/ocm" v1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" @@ -195,10 +196,11 @@ func (d *action) Save(o *elemhdlr.Object, f string) error { } var ok bool var eff string + printer := common2.NewPrinter(d.opts.Context.StdOut()) if local.UseHandlers { - ok, eff, err = d.downloaders.Download(d.opts.Context, racc, f, dest.PathFilesystem) + ok, eff, err = d.downloaders.Download(printer, racc, f, dest.PathFilesystem) } else { - ok, eff, err = d.downloaders.DownloadAsBlob(d.opts.Context, racc, f, dest.PathFilesystem) + ok, eff, err = d.downloaders.DownloadAsBlob(printer, racc, f, dest.PathFilesystem) } if err != nil { return err diff --git a/cmds/ocm/commands/ocmcmds/versions/show/cmd.go b/cmds/ocm/commands/ocmcmds/versions/show/cmd.go index 2669fc07b..28631943d 100644 --- a/cmds/ocm/commands/ocmcmds/versions/show/cmd.go +++ b/cmds/ocm/commands/ocmcmds/versions/show/cmd.go @@ -5,8 +5,6 @@ package show import ( - "sort" - "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -20,6 +18,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm" "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/out" + "github.com/open-component-model/ocm/pkg/semverutils" ) var ( @@ -139,21 +138,7 @@ func (o *Command) Run() error { } } - // Filter by patterns - for i := 0; i < len(versions); i++ { - found := len(o.Constraints) == 0 - for _, c := range o.Constraints { - if c.Check(versions[i]) { - found = true - } - } - if !found { - versions = append(versions[:i], versions[i+1:]...) - i-- - } - } - - sort.Sort(versions) + versions = semverutils.MatchVersions(versions, o.Constraints...) if len(versions) > 1 && o.Latest { versions = versions[len(versions)-1:] } diff --git a/cmds/ocm/commands/verbs/describe/cmd.go b/cmds/ocm/commands/verbs/describe/cmd.go index 1458de01f..17e2a5ce2 100644 --- a/cmds/ocm/commands/verbs/describe/cmd.go +++ b/cmds/ocm/commands/verbs/describe/cmd.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" resources "github.com/open-component-model/ocm/cmds/ocm/commands/ocicmds/artefacts/describe" + plugins "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/plugins/describe" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" "github.com/open-component-model/ocm/pkg/contexts/clictx" @@ -19,5 +20,6 @@ func NewCommand(ctx clictx.Context) *cobra.Command { Short: "Describe artefacts", }, verbs.Describe) cmd.AddCommand(resources.NewCommand(ctx)) + cmd.AddCommand(plugins.NewCommand(ctx)) return cmd } diff --git a/cmds/ocm/commands/verbs/install/cmd.go b/cmds/ocm/commands/verbs/install/cmd.go new file mode 100644 index 000000000..d5e1f61cd --- /dev/null +++ b/cmds/ocm/commands/verbs/install/cmd.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package install + +import ( + "github.com/spf13/cobra" + + plugins "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/plugins/install" + "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" + "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + "github.com/open-component-model/ocm/pkg/contexts/clictx" +) + +// NewCommand creates a new command. +func NewCommand(ctx clictx.Context) *cobra.Command { + cmd := utils.MassageCommand(&cobra.Command{ + Short: "Install elements.", + }, verbs.Install) + cmd.AddCommand(plugins.NewCommand(ctx)) + return cmd +} diff --git a/cmds/ocm/commands/verbs/verbs.go b/cmds/ocm/commands/verbs/verbs.go index c91c05db4..8e4786cb5 100644 --- a/cmds/ocm/commands/verbs/verbs.go +++ b/cmds/ocm/commands/verbs/verbs.go @@ -17,4 +17,5 @@ const ( Verify = "verify" Clean = "clean" Info = "info" + Install = "install" ) diff --git a/cmds/ocm/pkg/utils/command.go b/cmds/ocm/pkg/utils/command.go index 035cdb827..aca2daf7e 100644 --- a/cmds/ocm/pkg/utils/command.go +++ b/cmds/ocm/pkg/utils/command.go @@ -13,6 +13,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/pkg/options" "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/open-component-model/ocm/pkg/errors" ) // OCMCommand is a command pattern, that can be instantiated for a dediated @@ -189,7 +190,10 @@ func SetupCommand(ocmcmd OCMCommand, names ...string) *cobra.Command { out.Error(ocmcmd, err.Error()) } */ - return err + list := errors.ErrListf("") + list.Add(err) + list.Add(ocmcmd.OCMContext().Finalize()) + return list.Result() } if t, ok := ocmcmd.(CommandTweaker); ok { t.TweakCommand(c) diff --git a/cmds/ocm/topics/ocm/accessmethods/topic.go b/cmds/ocm/topics/ocm/accessmethods/topic.go new file mode 100644 index 000000000..063458830 --- /dev/null +++ b/cmds/ocm/topics/ocm/accessmethods/topic.go @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package topicocmaccessmethods + +import ( + "github.com/spf13/cobra" + + "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/open-component-model/ocm/pkg/contexts/ocm" +) + +func New(ctx clictx.Context) *cobra.Command { + return &cobra.Command{ + Use: "ocm-accessmethods", + Short: "List of all supported access methods", + + Long: ` +Access methods are used to handle the access to the content of artifacts +described in a component version. Therefore, an artifact entry contains +an access specification describing the access attributes for the dedicated +artifact. + +` + ocm.AccessUsage(ctx.OCMContext().AccessMethods(), true), + } +} diff --git a/docs/reference/ocm.md b/docs/reference/ocm.md index f411aefc5..15c81b9b0 100644 --- a/docs/reference/ocm.md +++ b/docs/reference/ocm.md @@ -140,6 +140,7 @@ attributes are supported: * [ocm describe](ocm_describe.md) — Describe artefacts * [ocm download](ocm_download.md) — Download oci artefacts, resources or complete components * [ocm get](ocm_get.md) — Get information about artefacts and components +* [ocm install](ocm_install.md) — Install elements. * [ocm show](ocm_show.md) — Show tags or versions * [ocm sign](ocm_sign.md) — Sign components * [ocm transfer](ocm_transfer.md) — Transfer artefacts or components @@ -153,5 +154,6 @@ attributes are supported: * [ocm attributes](ocm_attributes.md) — configuration attributes used to control the behaviour * [ocm configfile](ocm_configfile.md) — configuration file * [ocm oci-references](ocm_oci-references.md) — notation for OCI references +* [ocm ocm-accessmethods](ocm_ocm-accessmethods.md) — List of all supported access methods * [ocm ocm-references](ocm_ocm-references.md) — notation for OCM references * [ocm toi-bootstrapping](ocm_toi-bootstrapping.md) — Tiny OCM Installer based on component versions diff --git a/docs/reference/ocm_add_resource-configuration.md b/docs/reference/ocm_add_resource-configuration.md index 342c83bf6..406b3ac47 100644 --- a/docs/reference/ocm_add_resource-configuration.md +++ b/docs/reference/ocm_add_resource-configuration.md @@ -348,10 +348,12 @@ with the field type in the input field: Options used to configure fields: --inputCompress, --inputLibraries, --inputPath, --inputValues, --mediaType -The following access methods are known by the system. +The following list describes the supported access methods, their versions +and specification formats. Typically there is special support for the CLI artifact add commands. -The following types (with the field type in the access field -are handled: +The access method specification can be put below the access field. +If always requires the field type describing the kind and version +shown below. - Access type S3 diff --git a/docs/reference/ocm_add_resources.md b/docs/reference/ocm_add_resources.md index c0bc32d3d..3cbe73732 100644 --- a/docs/reference/ocm_add_resources.md +++ b/docs/reference/ocm_add_resources.md @@ -343,10 +343,12 @@ with the field type in the input field: Options used to configure fields: --inputCompress, --inputLibraries, --inputPath, --inputValues, --mediaType -The following access methods are known by the system. +The following list describes the supported access methods, their versions +and specification formats. Typically there is special support for the CLI artifact add commands. -The following types (with the field type in the access field -are handled: +The access method specification can be put below the access field. +If always requires the field type describing the kind and version +shown below. - Access type S3 diff --git a/docs/reference/ocm_add_source-configuration.md b/docs/reference/ocm_add_source-configuration.md index bd71a9cde..98507705e 100644 --- a/docs/reference/ocm_add_source-configuration.md +++ b/docs/reference/ocm_add_source-configuration.md @@ -348,10 +348,12 @@ with the field type in the input field: Options used to configure fields: --inputCompress, --inputLibraries, --inputPath, --inputValues, --mediaType -The following access methods are known by the system. +The following list describes the supported access methods, their versions +and specification formats. Typically there is special support for the CLI artifact add commands. -The following types (with the field type in the access field -are handled: +The access method specification can be put below the access field. +If always requires the field type describing the kind and version +shown below. - Access type S3 diff --git a/docs/reference/ocm_add_sources.md b/docs/reference/ocm_add_sources.md index 255bc7139..37744ed6a 100644 --- a/docs/reference/ocm_add_sources.md +++ b/docs/reference/ocm_add_sources.md @@ -341,10 +341,12 @@ with the field type in the input field: Options used to configure fields: --inputCompress, --inputLibraries, --inputPath, --inputValues, --mediaType -The following access methods are known by the system. +The following list describes the supported access methods, their versions +and specification formats. Typically there is special support for the CLI artifact add commands. -The following types (with the field type in the access field -are handled: +The access method specification can be put below the access field. +If always requires the field type describing the kind and version +shown below. - Access type S3 diff --git a/docs/reference/ocm_configfile.md b/docs/reference/ocm_configfile.md index ca046491e..cad080e20 100644 --- a/docs/reference/ocm_configfile.md +++ b/docs/reference/ocm_configfile.md @@ -140,6 +140,15 @@ The following configuration types are supported: <name>: <specification of OCI registry> ... +- plugin.config.ocm.software + The config type plugin.config.ocm.software can be used to configure a + plugin. + +
+      type: plugin.config.ocm.software
+      plugin: <plugin name>
+      config: <arbitrary configuration structure>
+  
- scripts.ocm.config.ocm.software The config type scripts.ocm.config.ocm.software can be used to define transfer scripts: diff --git a/docs/reference/ocm_describe.md b/docs/reference/ocm_describe.md index a0c1325bc..3ab48abb5 100644 --- a/docs/reference/ocm_describe.md +++ b/docs/reference/ocm_describe.md @@ -22,4 +22,5 @@ ocm describe [] ... ##### Sub Commands * [ocm describe artefacts](ocm_describe_artefacts.md) — describe artefact version +* [ocm describe plugins](ocm_describe_plugins.md) — get plugins diff --git a/docs/reference/ocm_describe_plugins.md b/docs/reference/ocm_describe_plugins.md new file mode 100644 index 000000000..7e11c10e0 --- /dev/null +++ b/docs/reference/ocm_describe_plugins.md @@ -0,0 +1,35 @@ +## ocm describe plugins — Get Plugins + +### Synopsis + +``` +ocm describe plugins [] {} +``` + +### Options + +``` + -h, --help help for plugins +``` + +### Description + + +Describes provides comprehensive information about the capabilities of +a plugin. + + +### Examples + +``` +$ ocm describe plugins +$ ocm describe plugins demo +``` + +### SEE ALSO + +##### Parents + +* [ocm describe](ocm_describe.md) — Describe artefacts +* [ocm](ocm.md) — Open Component Model command line client + diff --git a/docs/reference/ocm_get_plugins.md b/docs/reference/ocm_get_plugins.md index 64f9c9fc7..0d68af537 100644 --- a/docs/reference/ocm_get_plugins.md +++ b/docs/reference/ocm_get_plugins.md @@ -11,7 +11,6 @@ ocm get plugins [] {} ``` -h, --help help for plugins -o, --output string output mode (JSON, json, wide, yaml) - --repo string repository name or spec -s, --sort stringArray sort fields ``` @@ -21,51 +20,6 @@ ocm get plugins [] {} Get lists information for all plugins specified, if no plugin is specified all registered ones are listed. -If the --repo option is specified, the given names are interpreted -relative to the specified repository using the syntax - -
-
<component>[:<version>]
-
- -If no --repo option is specified the given names are interpreted -as located OCM component version references: - -
-
[<repo type>::]<host>[:<port>][/<base path>]//<component>[:<version>]
-
- -Additionally there is a variant to denote common transport archives -and general repository specifications - -
-
[<repo type>::]<filepath>|<spec json>[//<component>[:<version>]]
-
- -The --repo option takes an OCM repository specification: - -
-
[<repo type>::]<configured name>|<file path>|<spec json>
-
- -For the *Common Transport Format* the types directory, -tar or tgz is possible. - -Using the JSON variant any repository type supported by the -linked library can be used: - -Dedicated OCM repository types: -- `ComponentArchive` - -OCI Repository types (using standard component repository to OCI mapping): -- `ArtefactSet` -- `CommonTransportFormat` -- `DockerDaemon` -- `Empty` -- `OCIRegistry` -- `oci` -- `ociRegistry` - With the option --output the output mode can be selected. The following modes are supported: - JSON diff --git a/docs/reference/ocm_install.md b/docs/reference/ocm_install.md new file mode 100644 index 000000000..910ae8dec --- /dev/null +++ b/docs/reference/ocm_install.md @@ -0,0 +1,25 @@ +## ocm install — Install Elements. + +### Synopsis + +``` +ocm install [] ... +``` + +### Options + +``` + -h, --help help for install +``` + +### SEE ALSO + +##### Parents + +* [ocm](ocm.md) — Open Component Model command line client + + +##### Sub Commands + +* [ocm install plugins](ocm_install_plugins.md) — install or update an OCM plugin + diff --git a/docs/reference/ocm_install_plugins.md b/docs/reference/ocm_install_plugins.md new file mode 100644 index 000000000..f1382e70d --- /dev/null +++ b/docs/reference/ocm_install_plugins.md @@ -0,0 +1,90 @@ +## ocm install plugins — Install Or Update An OCM Plugin + +### Synopsis + +``` +ocm install plugins [] [] | +``` + +### Options + +``` + -c, --constraints constraints version constraint + -d, --describe describe plugin, only + -f, --force overwrite existing plugin + -h, --help help for plugins + -r, --remove remove plugin + -u, --update update plugin +``` + +### Description + + +Download and install a plugin provided by an OCM component version. +For the update mode only the plugin name is required. + +If no version is specified the latest version is chosen. If at least one +version constraint is given, only the matching versions are considered. + +If the --repo option is specified, the given names are interpreted +relative to the specified repository using the syntax + +
+
<component>[:<version>]
+
+ +If no --repo option is specified the given names are interpreted +as located OCM component version references: + +
+
[<repo type>::]<host>[:<port>][/<base path>]//<component>[:<version>]
+
+ +Additionally there is a variant to denote common transport archives +and general repository specifications + +
+
[<repo type>::]<filepath>|<spec json>[//<component>[:<version>]]
+
+ +The --repo option takes an OCM repository specification: + +
+
[<repo type>::]<configured name>|<file path>|<spec json>
+
+ +For the *Common Transport Format* the types directory, +tar or tgz is possible. + +Using the JSON variant any repository type supported by the +linked library can be used: + +Dedicated OCM repository types: +- `ComponentArchive` + +OCI Repository types (using standard component repository to OCI mapping): +- `ArtefactSet` +- `CommonTransportFormat` +- `DockerDaemon` +- `Empty` +- `OCIRegistry` +- `oci` +- `ociRegistry` + + +### Examples + +``` +$ ocm install plugin ghcr.io/github.com/mandelsoft/cnudie//github.com/mandelsoft/ocmplugin:0.1.0-dev +$ ocm install plugin -c 1.2.x ghcr.io/github.com/mandelsoft/cnudie//github.com/mandelsoft/ocmplugin +$ ocm install plugin -u demo +$ ocm install plugin -r demo +``` + +### SEE ALSO + +##### Parents + +* [ocm install](ocm_install.md) — Install elements. +* [ocm](ocm.md) — Open Component Model command line client + diff --git a/docs/reference/ocm_ocm-accessmethods.md b/docs/reference/ocm_ocm-accessmethods.md new file mode 100644 index 000000000..8f04bc0cd --- /dev/null +++ b/docs/reference/ocm_ocm-accessmethods.md @@ -0,0 +1,181 @@ +## ocm ocm-accessmethods — List Of All Supported Access Methods + +### Description + + +Access methods are used to handle the access to the content of artifacts +described in a component version. Therefore, an artifact entry contains +an access specification describing the access attributes for the dedicated +artifact. + + +The following list describes the supported access methods, their versions +and specification formats. +Typically there is special support for the CLI artifact add commands. +The access method specification can be put below the access field. +If always requires the field type describing the kind and version +shown below. + +- Access type S3 + + This method implements the access of a blob stored in an S3 bucket. + + The following versions are supported: + - Version v1 + + The type specific specification fields are: + + - **region** (optional) *string* + + OCI repository reference (this artefact name used to store the blob). + + - **bucket** *string* + + The name of the S3 bucket containing the blob + + - **key** *string* + + The key of the desired blob + + Options used to configure fields: --accessVersion, --bucket, --mediaType, --reference, --region + + +- Access type gitHub + + This method implements the access of the content of a git commit stored in a + GitHub repository. + + The following versions are supported: + - Version v1 + + The type specific specification fields are: + + - **repoUrl** *string* + + Repository URL with or without scheme. + + - **ref** (optional) *string* + + Original ref used to get the commit from + + - **commit** *string* + + The sha/id of the git commit + + Options used to configure fields: --accessHostname, --accessRepository, --commit + + +- Access type localBlob + + This method is used to store a resource blob along with the component descriptor + on behalf of the hosting OCM repository. + + Its implementation is specific to the implementation of OCM + repository used to read the component descriptor. Every repository + implementation may decide how and where local blobs are stored, + but it MUST provide an implementation for this method. + + Regardless of the chosen implementation the attribute specification is + defined globally the same. + + The following versions are supported: + - Version v1 + + The type specific specification fields are: + + - **localReference** *string* + + Repository type specific location information as string. The value + may encode any deep structure, but typically just an access path is sufficient. + + - **mediaType** *string* + + The media type of the blob used to store the resource. It may add + format information like +tar or +gzip. + + - **referenceName** (optional) *string* + + This optional attribute may contain identity information used by + other repositories to restore some global access with an identity + related to the original source. + + For example, if an OCI artefact originally referenced using the + access method [ociArtefact](../../../../../docs/formats/accessmethods/ociArtefact.md) is stored during + some transport step as local artefact, the reference name can be set + to its original repository name. An import step into an OCI based OCM + repository may then decide to make this artefact available again as + regular OCI artefact. + + - **globalAccess** (optional) *access method specification* + + If a resource blob is stored locally, the repository implementation + may decide to provide an external access information (independent + of the OCM model). + + For example, an OCI artefact stored as local blob + can be additionally stored as regular OCI artefact in an OCI registry. + + This additional external access information can be added using + a second external access method specification. + + Options used to configure fields: --globalAccess, --hint, --mediaType, --reference + + +- Access type none + + dummy resource with no access + + +- Access type ociArtefact + + This method implements the access of an OCI artefact stored in an OCI registry. + + The following versions are supported: + - Version v1 + + The type specific specification fields are: + + - **imageReference** *string* + + OCI image/artefact reference following the possible docker schemes: + - <repo>/<artefact>:<digest>@<tag> + - [<port>]/<repo path>/<artefact>:<version>@<tag> + + Options used to configure fields: --reference + + +- Access type ociBlob + + This method implements the access of an OCI blob stored in an OCI repository. + + The following versions are supported: + - Version v1 + + The type specific specification fields are: + + - **imageReference** *string* + + OCI repository reference (this artefact name used to store the blob). + + - **mediaType** *string* + + The media type of the blob + + - **digest** *string* + + The digest of the blob used to access the blob in the OCI repository. + + - **size** *integer* + + The size of the blob + + Options used to configure fields: --digest, --mediaType, --reference, --size + + + +### SEE ALSO + +##### Parents + +* [ocm](ocm.md) — Open Component Model command line client + diff --git a/docs/reference/ocm_transfer_commontransportarchive.md b/docs/reference/ocm_transfer_commontransportarchive.md index b4adbf514..8710de08b 100644 --- a/docs/reference/ocm_transfer_commontransportarchive.md +++ b/docs/reference/ocm_transfer_commontransportarchive.md @@ -9,12 +9,13 @@ ocm transfer commontransportarchive [] ### Options ``` - -V, --copy-resources transfer referenced resources by-value - -h, --help help for commontransportarchive - -f, --overwrite overwrite existing component versions - --script string config name of transfer handler script - -s, --scriptFile string filename of transfer handler script - -t, --type string archive format (directory, tar, tgz) (default "directory") + -V, --copy-resources transfer referenced resources by-value + -h, --help help for commontransportarchive + -f, --overwrite overwrite existing component versions + --script string config name of transfer handler script + -s, --scriptFile string filename of transfer handler script + -t, --type string archive format (directory, tar, tgz) (default "directory") + --uploader = repository uploader (::=script
option family. +If the --uploader option is specified, appropriate uploaders +are configured for the transport target. It has the following format + +
+
<name>:<artefact type>:<media type>=<yaml target config>
+
+ +The uploader name may be a path expression with the following possibilities: +- ocm/ociRegistry: oci Registry upload for local OCI artefact blobs. + The media type is optional. If given ist must be an OCI artefact media type. +- plugin/[/: uploader provided by plugin. + It is possible to use a dedicated transfer script based on spiff. The option --scriptFile can be used to specify this script by a file name. With --script it can be taken from the diff --git a/docs/reference/ocm_transfer_componentversions.md b/docs/reference/ocm_transfer_componentversions.md index f8b46cb34..fd3f47346 100644 --- a/docs/reference/ocm_transfer_componentversions.md +++ b/docs/reference/ocm_transfer_componentversions.md @@ -9,15 +9,16 @@ ocm transfer componentversions [] {} ### Options ``` - -V, --copy-resources transfer referenced resources by-value - -h, --help help for componentversions - --lookup stringArray repository name or spec for closure lookup fallback - -f, --overwrite overwrite existing component versions - -r, --recursive follow component reference nesting - --repo string repository name or spec - --script string config name of transfer handler script - -s, --scriptFile string filename of transfer handler script - -t, --type string archive format (directory, tar, tgz) (default "directory") + -V, --copy-resources transfer referenced resources by-value + -h, --help help for componentversions + --lookup stringArray repository name or spec for closure lookup fallback + -f, --overwrite overwrite existing component versions + -r, --recursive follow component reference nesting + --repo string repository name or spec + --script string config name of transfer handler script + -s, --scriptFile string filename of transfer handler script + -t, --type string archive format (directory, tar, tgz) (default "directory") + --uploader = repository uploader (::=script option family. +If the --uploader option is specified, appropriate uploaders +are configured for the transport target. It has the following format + +
+
<name>:<artefact type>:<media type>=<yaml target config>
+
+ +The uploader name may be a path expression with the following possibilities: +- ocm/ociRegistry: oci Registry upload for local OCI artefact blobs. + The media type is optional. If given ist must be an OCI artefact media type. +- plugin/[/: uploader provided by plugin. + It is possible to use a dedicated transfer script based on spiff. The option --scriptFile can be used to specify this script by a file name. With --script it can be taken from the diff --git a/pkg/cobrautils/flag/semver.go b/pkg/cobrautils/flag/semver.go new file mode 100644 index 000000000..96c33a85a --- /dev/null +++ b/pkg/cobrautils/flag/semver.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package flag + +import ( + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/spf13/pflag" +) + +type semverValue struct { + value *[]*semver.Version + changed bool +} + +func newSemverValue(val []*semver.Version, p *[]*semver.Version) *semverValue { + ssv := new(semverValue) + ssv.value = p + *ssv.value = val + return ssv +} + +func (s *semverValue) Set(val string) error { + c, err := semver.NewVersion(val) + if err != nil { + return err + } + if !s.changed { + *s.value = []*semver.Version{c} + } else { + *s.value = append(*s.value, c) + } + s.changed = true + return nil +} + +func (s *semverValue) Type() string { + return "[]semver" +} + +func (s *semverValue) String() string { + if *s.value == nil { + return "" + } + var list []string + for _, v := range *s.value { + list = append(list, v.String()) + } + return "[" + strings.Join(list, ", ") + "]" +} + +func SemverVar(f *pflag.FlagSet, p *[]*semver.Version, name string, value []*semver.Version, usage string) { + f.VarP(newSemverValue(value, p), name, "", usage) +} + +func SemverVarP(f *pflag.FlagSet, p *[]*semver.Version, name, shorthand string, value []*semver.Version, usage string) { + f.VarP(newSemverValue(value, p), name, shorthand, usage) +} + +func SemverVarPF(f *pflag.FlagSet, p *[]*semver.Version, name, shorthand string, value []*semver.Version, usage string) *pflag.Flag { + return f.VarPF(newSemverValue(value, p), name, shorthand, usage) +} + +func Semver(f *pflag.FlagSet, name string, value []*semver.Version, usage string) *[]*semver.Version { + p := []*semver.Version{} + SemverVarP(f, &p, name, "", value, usage) + return &p +} + +func SemverP(f *pflag.FlagSet, name, shorthand string, value []*semver.Version, usage string) *[]*semver.Version { + p := []*semver.Version{} + SemverVarP(f, &p, name, shorthand, value, usage) + return &p +} diff --git a/pkg/cobrautils/flag/semver_constraint.go b/pkg/cobrautils/flag/semver_constraint.go new file mode 100644 index 000000000..2987e963c --- /dev/null +++ b/pkg/cobrautils/flag/semver_constraint.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package flag + +import ( + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/spf13/pflag" +) + +type constaintsValue struct { + value *[]*semver.Constraints + changed bool +} + +func newConstraintsValue(val []*semver.Constraints, p *[]*semver.Constraints) *constaintsValue { + ssv := new(constaintsValue) + ssv.value = p + *ssv.value = val + return ssv +} + +func (s *constaintsValue) Set(val string) error { + c, err := semver.NewConstraint(val) + if err != nil { + return err + } + if !s.changed { + *s.value = []*semver.Constraints{c} + } else { + *s.value = append(*s.value, c) + } + s.changed = true + return nil +} + +func (s *constaintsValue) Type() string { + return "constraints" +} + +func (s *constaintsValue) String() string { + if *s.value == nil { + return "" + } + var list []string + for _, v := range *s.value { + list = append(list, v.String()) + } + return "[" + strings.Join(list, ", ") + "]" +} + +func SemverConstraintsVar(f *pflag.FlagSet, p *[]*semver.Constraints, name string, value []*semver.Constraints, usage string) { + f.VarP(newConstraintsValue(value, p), name, "", usage) +} + +func SemverConstraintsVarP(f *pflag.FlagSet, p *[]*semver.Constraints, name, shorthand string, value []*semver.Constraints, usage string) { + f.VarP(newConstraintsValue(value, p), name, shorthand, usage) +} + +func SemverConstraintsVarPF(f *pflag.FlagSet, p *[]*semver.Constraints, name, shorthand string, value []*semver.Constraints, usage string) *pflag.Flag { + return f.VarPF(newConstraintsValue(value, p), name, shorthand, usage) +} + +func SemverConstraints(f *pflag.FlagSet, name string, value []*semver.Constraints, usage string) *[]*semver.Constraints { + p := []*semver.Constraints{} + SemverConstraintsVarP(f, &p, name, "", value, usage) + return &p +} + +func SemverConstraintsP(f *pflag.FlagSet, name, shorthand string, value []*semver.Constraints, usage string) *[]*semver.Constraints { + p := []*semver.Constraints{} + SemverConstraintsVarP(f, &p, name, shorthand, value, usage) + return &p +} diff --git a/pkg/cobrautils/flag/string_to_string.go b/pkg/cobrautils/flag/string_to_string.go index 43ed78555..c20948070 100644 --- a/pkg/cobrautils/flag/string_to_string.go +++ b/pkg/cobrautils/flag/string_to_string.go @@ -57,6 +57,9 @@ func (s *stringToStringValue[T]) Set(val string) error { if !s.changed { *s.value = out } else { + if *s.value == nil { + *s.value = map[string]string{} + } for k, v := range out { (*s.value)[k] = v } diff --git a/pkg/cobrautils/flagsets/configoptionset.go b/pkg/cobrautils/flagsets/configoptionset.go index 92c610493..5247f5a5d 100644 --- a/pkg/cobrautils/flagsets/configoptionset.go +++ b/pkg/cobrautils/flagsets/configoptionset.go @@ -296,7 +296,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 not match (%T<->%T)", o.GetName(), o, old) + return fmt.Errorf("option type %s doesn't match (%T<->%T)", o.GetName(), o, old) } } return nil diff --git a/pkg/cobrautils/flagsets/types.go b/pkg/cobrautils/flagsets/types.go index b762f5060..556fd8aa5 100644 --- a/pkg/cobrautils/flagsets/types.go +++ b/pkg/cobrautils/flagsets/types.go @@ -87,6 +87,16 @@ func NewStringOptionType(name string, description string) ConfigOptionType { } } +func (s *StringOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *StringOptionType) Create() Option { + return &StringOption{ + OptionBase: NewOptionBase(s), + } +} + type StringOption struct { OptionBase value string @@ -102,16 +112,6 @@ func (o *StringOption) Value() interface{} { return o.value } -func (s *StringOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *StringOptionType) Create() Option { - return &StringOption{ - OptionBase: NewOptionBase(s), - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type StringArrayOptionType struct { @@ -124,6 +124,16 @@ func NewStringArrayOptionType(name string, description string) ConfigOptionType } } +func (s *StringArrayOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *StringArrayOptionType) Create() Option { + return &StringArrayOption{ + OptionBase: NewOptionBase(s), + } +} + type StringArrayOption struct { OptionBase value []string @@ -139,16 +149,6 @@ func (o *StringArrayOption) Value() interface{} { return o.value } -func (s *StringArrayOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *StringArrayOptionType) Create() Option { - return &StringArrayOption{ - OptionBase: NewOptionBase(s), - } -} - //////////////////////////////////////////////////////////////////////////////// type BoolOptionType struct { @@ -161,6 +161,16 @@ func NewBoolOptionType(name string, description string) ConfigOptionType { } } +func (s *BoolOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *BoolOptionType) Create() Option { + return &BoolOption{ + OptionBase: NewOptionBase(s), + } +} + type BoolOption struct { OptionBase value bool @@ -176,16 +186,6 @@ func (o *BoolOption) Value() interface{} { return o.value } -func (s *BoolOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *BoolOptionType) Create() Option { - return &BoolOption{ - OptionBase: NewOptionBase(s), - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type IntOptionType struct { @@ -198,6 +198,16 @@ func NewIntOptionType(name string, description string) ConfigOptionType { } } +func (s *IntOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *IntOptionType) Create() Option { + return &IntOption{ + OptionBase: NewOptionBase(s), + } +} + type IntOption struct { OptionBase value int @@ -213,16 +223,6 @@ func (o *IntOption) Value() interface{} { return o.value } -func (s *IntOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *IntOptionType) Create() Option { - return &IntOption{ - OptionBase: NewOptionBase(s), - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type YAMLOptionType struct { @@ -235,6 +235,16 @@ func NewYAMLOptionType(name string, description string) ConfigOptionType { } } +func (s *YAMLOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *YAMLOptionType) Create() Option { + return &YAMLOption{ + OptionBase: NewOptionBase(s), + } +} + type YAMLOption struct { OptionBase value interface{} @@ -250,16 +260,6 @@ func (o *YAMLOption) Value() interface{} { return o.value } -func (s *YAMLOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *YAMLOptionType) Create() Option { - return &YAMLOption{ - OptionBase: NewOptionBase(s), - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type ValueMapYAMLOptionType struct { @@ -272,6 +272,16 @@ func NewValueMapYAMLOptionType(name string, description string) ConfigOptionType } } +func (s *ValueMapYAMLOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *ValueMapYAMLOptionType) Create() Option { + return &YAMLOption{ + OptionBase: NewOptionBase(s), + } +} + type ValueMapYAMLOption struct { OptionBase value map[string]interface{} @@ -287,16 +297,6 @@ func (o *ValueMapYAMLOption) Value() interface{} { return o.value } -func (s *ValueMapYAMLOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *ValueMapYAMLOptionType) Create() Option { - return &YAMLOption{ - OptionBase: NewOptionBase(s), - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type ValueMapOptionType struct { @@ -309,6 +309,16 @@ func NewValueMapOptionType(name string, description string) ConfigOptionType { } } +func (s *ValueMapOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *ValueMapOptionType) Create() Option { + return &ValueMapOption{ + OptionBase: NewOptionBase(s), + } +} + type ValueMapOption struct { OptionBase value map[string]interface{} @@ -324,16 +334,6 @@ func (o *ValueMapOption) Value() interface{} { return o.value } -func (s *ValueMapOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *ValueMapOptionType) Create() Option { - return &ValueMapOption{ - OptionBase: NewOptionBase(s), - } -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// type StringMapOptionType struct { @@ -346,6 +346,16 @@ func NewStringMapOptionType(name string, description string) ConfigOptionType { } } +func (s *StringMapOptionType) Equal(optionType ConfigOptionType) bool { + return reflect.DeepEqual(s, optionType) +} + +func (s *StringMapOptionType) Create() Option { + return &StringMapOption{ + OptionBase: NewOptionBase(s), + } +} + type StringMapOption struct { OptionBase value map[string]string @@ -360,13 +370,3 @@ func (o *StringMapOption) AddFlags(fs *pflag.FlagSet) { func (o *StringMapOption) Value() interface{} { return o.value } - -func (s *StringMapOptionType) Equal(optionType ConfigOptionType) bool { - return reflect.DeepEqual(s, optionType) -} - -func (s *StringMapOptionType) Create() Option { - return &StringMapOption{ - OptionBase: NewOptionBase(s), - } -} diff --git a/pkg/cobrautils/tweak.go b/pkg/cobrautils/tweak.go index d53f8b561..f55249840 100644 --- a/pkg/cobrautils/tweak.go +++ b/pkg/cobrautils/tweak.go @@ -58,15 +58,15 @@ func SupportNestedHelpFunc(cmd *cobra.Command) { func CleanMarkdownUsageFunc(cmd *cobra.Command) { defaultHelpFunc := cmd.HelpFunc() cmd.SetHelpFunc(func(cmd *cobra.Command, s []string) { - cmd.Long = cleanMarkdown(cmd.Long) - cmd.Example = cleanMarkdown(cmd.Example) + cmd.Long = CleanMarkdown(cmd.Long) + cmd.Example = CleanMarkdown(cmd.Example) defaultHelpFunc(cmd, s) }) } var center = regexp.MustCompile(" * *\n?") -func cleanMarkdown(s string) string { +func CleanMarkdown(s string) string { if strings.HasPrefix(s, "##") { for strings.HasPrefix(s, "#") { s = s[1:] @@ -82,5 +82,33 @@ func cleanMarkdown(s string) string { s = strings.ReplaceAll(s, "", "") s = strings.ReplaceAll(s, "", "") s = string(center.ReplaceAll([]byte(s), nil)) - return s + + var r []string + found := false + mask := false + for _, l := range strings.Split(s, "\n") { + if strings.Contains(l, "
") {
+			mask = true
+		}
+		if strings.Contains(l, "
") { + mask = false + } + if !mask { + if strings.HasSuffix(l, "\\") { + l = l[:len(l)-1] + found = true + } else { + if strings.TrimSpace(l) == "" { + if !found { + found = true + continue + } + } else { + found = false + } + } + } + r = append(r, l) + } + return strings.Join(r, "\n") } diff --git a/pkg/common/printer.go b/pkg/common/printer.go index 556c4609f..3872c545f 100644 --- a/pkg/common/printer.go +++ b/pkg/common/printer.go @@ -76,7 +76,7 @@ func (p *printer) Write(data []byte) (int, error) { } func (p *printer) printf(msg string, args ...interface{}) (int, error) { - if p.writer == nil { + if p == nil || p.writer == nil { return 0, nil } if p.gap == "" { diff --git a/pkg/contexts/oci/repositories/ocireg/namespace.go b/pkg/contexts/oci/repositories/ocireg/namespace.go index 3b777baef..723bd8c2c 100644 --- a/pkg/contexts/oci/repositories/ocireg/namespace.go +++ b/pkg/contexts/oci/repositories/ocireg/namespace.go @@ -80,7 +80,6 @@ func (n *NamespaceContainer) getPusher(vers string) (resolve.Pusher, error) { resolver := n.resolver n.repo.ctx.Logger().Trace("get pusher", "ref", ref) - if ok, _ := artdesc.IsDigest(vers); !ok { var err error diff --git a/pkg/contexts/ocm/accessmethods/options/types.go b/pkg/contexts/ocm/accessmethods/options/types.go index 467222633..2782e7966 100644 --- a/pkg/contexts/ocm/accessmethods/options/types.go +++ b/pkg/contexts/ocm/accessmethods/options/types.go @@ -6,7 +6,6 @@ package options import ( "fmt" - "reflect" "github.com/open-component-model/ocm/pkg/cobrautils/flagsets" ) @@ -14,6 +13,7 @@ import ( type OptionType interface { flagsets.ConfigOptionType ValueType() string + GetDescriptionText() string } type base = flagsets.ConfigOptionType @@ -24,7 +24,10 @@ type option struct { } func (o *option) Equal(t flagsets.ConfigOptionType) bool { - return reflect.DeepEqual(o, t) + if ot, ok := t.(*option); ok { + return o.valueType == ot.valueType && o.GetName() == ot.GetName() + } + return false } func (o *option) ValueType() string { @@ -35,6 +38,10 @@ 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 { diff --git a/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go b/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go index e465e22de..facf90506 100644 --- a/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go +++ b/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go @@ -18,6 +18,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" + "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/comparch" ) @@ -35,7 +36,7 @@ var _ = Describe("Add with new access method", func() { plugindirattr.Set(ctx, "testdata") registry = plugincacheattr.Get(ctx) - Expect(registry.RegisterExtensions()).To(Succeed()) + Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") Expect(p).NotTo(BeNil()) diff --git a/pkg/contexts/ocm/accessmethods/plugin/method_test.go b/pkg/contexts/ocm/accessmethods/plugin/method_test.go index d47c7f0b5..b027178b0 100644 --- a/pkg/contexts/ocm/accessmethods/plugin/method_test.go +++ b/pkg/contexts/ocm/accessmethods/plugin/method_test.go @@ -19,6 +19,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugindirattr" metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" + "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" ) @@ -47,7 +48,7 @@ someattr: value ctx = env.OCMContext() plugindirattr.Set(ctx, "testdata") registry = plugincacheattr.Get(ctx) - Expect(registry.RegisterExtensions()).To(Succeed()) + Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") Expect(p).NotTo(BeNil()) }) diff --git a/pkg/contexts/ocm/attrs/ociuploadattr/attr.go b/pkg/contexts/ocm/attrs/ociuploadattr/attr.go index ef3324715..172c62f7e 100644 --- a/pkg/contexts/ocm/attrs/ociuploadattr/attr.go +++ b/pkg/contexts/ocm/attrs/ociuploadattr/attr.go @@ -50,10 +50,16 @@ func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (in var value Attribute err := unmarshaller.Unmarshal(data, &value) if err == nil { - if value.Repository.GetType() == "" { - return nil, errors.ErrInvalidWrap(errors.Newf("missing repository type"), oci.KIND_OCI_REFERENCE, string(data)) + if value.Repository != nil { + if value.Repository.GetType() == "" { + return nil, errors.ErrInvalidWrap(errors.Newf("missing repository type"), oci.KIND_OCI_REFERENCE, string(data)) + } + return &value, nil } - return &value, nil + if value.Ref == "" { + return nil, errors.ErrInvalidWrap(errors.Newf("missing repository or ref"), oci.KIND_OCI_REFERENCE, string(data)) + } + data = []byte(value.Ref) } ref, err := oci.ParseRef(string(data)) if err != nil { @@ -136,6 +142,7 @@ func (a *Attribute) getBySpec(ctx cpi.Context) (oci.Repository, *oci.UniformRepo a.prefix = a.NamespacePrefix a.spec = data a.ref = &oci.RefSpec{UniformRepositorySpec: *spec.UniformRepositorySpec()} + ctx.Finalizer().Close(a) } return a.repo, &a.ref.UniformRepositorySpec, a.prefix, nil } @@ -167,6 +174,7 @@ func (a *Attribute) getByRef(ctx cpi.Context) (oci.Repository, *oci.UniformRepos } a.prefix = ref.Repository a.ref = &ref + ctx.Finalizer().Close(a) } return a.repo, &a.ref.UniformRepositorySpec, a.prefix, nil } diff --git a/pkg/contexts/ocm/attrs/plugincacheattr/attr.go b/pkg/contexts/ocm/attrs/plugincacheattr/attr.go index 94026cd93..0a365e914 100644 --- a/pkg/contexts/ocm/attrs/plugincacheattr/attr.go +++ b/pkg/contexts/ocm/attrs/plugincacheattr/attr.go @@ -5,15 +5,11 @@ package plugincacheattr import ( - "encoding/json" - "github.com/open-component-model/ocm/pkg/contexts/datacontext" "github.com/open-component-model/ocm/pkg/contexts/ocm" "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugindirattr" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" - "github.com/open-component-model/ocm/pkg/errors" ) const ( @@ -34,19 +30,3 @@ func Get(ctx ocm.Context) plugins.Set { func Set(ctx ocm.Context, cache cache.PluginDir) error { return ctx.GetAttributes().SetAttribute(ATTR_KEY, cache) } - -func RegisterBlobHandler(ctx ocm.Context, pname, name string, artType, mediaType string, target json.RawMessage) error { - set := Get(ctx) - if set == nil { - return errors.ErrUnknown(plugin.KIND_PLUGIN, pname) - } - return set.RegisterBlobHandler(pname, name, artType, mediaType, target) -} - -func RegisterDownloadHandler(ctx ocm.Context, pname, name string, artType, mediaType string) error { - set := Get(ctx) - if set == nil { - return errors.ErrUnknown(plugin.KIND_PLUGIN, pname) - } - return set.RegisterDownloadHandler(pname, name, artType, mediaType) -} diff --git a/pkg/contexts/ocm/blobhandler/generic/ocirepo/blobhandler.go b/pkg/contexts/ocm/blobhandler/generic/ocirepo/blobhandler.go index 8e8d39de4..5ccacb267 100644 --- a/pkg/contexts/ocm/blobhandler/generic/ocirepo/blobhandler.go +++ b/pkg/contexts/ocm/blobhandler/generic/ocirepo/blobhandler.go @@ -18,6 +18,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/ociuploadattr" "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" "github.com/open-component-model/ocm/pkg/errors" + "github.com/open-component-model/ocm/pkg/utils" ) func init() { @@ -29,14 +30,19 @@ func init() { //////////////////////////////////////////////////////////////////////////////// // artefactHandler stores artefact blobs as OCIArtefacts. -type artefactHandler struct{} +type artefactHandler struct { + spec *ociuploadattr.Attribute +} -func NewArtefactHandler() cpi.BlobHandler { - return &artefactHandler{} +func NewArtefactHandler(repospec ...*ociuploadattr.Attribute) cpi.BlobHandler { + return &artefactHandler{utils.Optional(repospec...)} } func (b *artefactHandler) StoreBlob(blob cpi.BlobAccess, artType, hint string, global cpi.AccessSpec, ctx cpi.StorageContext) (cpi.AccessSpec, error) { - attr := ociuploadattr.Get(ctx.GetContext()) + attr := b.spec + if attr == nil { + attr = ociuploadattr.Get(ctx.GetContext()) + } if attr == nil { return nil, nil } diff --git a/pkg/contexts/ocm/blobhandler/generic/ocirepo/registration.go b/pkg/contexts/ocm/blobhandler/generic/ocirepo/registration.go new file mode 100644 index 000000000..2d94e79bf --- /dev/null +++ b/pkg/contexts/ocm/blobhandler/generic/ocirepo/registration.go @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package ocirepo + +import ( + "encoding/json" + "fmt" + + "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/ociuploadattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "github.com/open-component-model/ocm/pkg/errors" + "github.com/open-component-model/ocm/pkg/runtime" +) + +type Config = ociuploadattr.Attribute + +func init() { + cpi.RegisterBlobHandlerRegistrationHandler("ocm/ociArtifacts", &RegistrationHandler{}) +} + +type RegistrationHandler struct{} + +var _ cpi.BlobHandlerRegistrationHandler = (*RegistrationHandler)(nil) + +func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config cpi.BlobHandlerConfig, olist ...cpi.BlobHandlerOption) (bool, error) { + if handler != "" { + return true, fmt.Errorf("invalid ociArtefact handler %q", handler) + } + if config == nil { + return true, fmt.Errorf("oci target specification required") + } + var attr *Config + switch a := config.(type) { + case *ociuploadattr.Attribute: + attr = a + case json.RawMessage: + r, err := ociuploadattr.AttributeType{}.Decode(a, runtime.DefaultYAMLEncoding) + if err != nil { + return true, errors.Wrapf(err, "cannot unmarshal blob handler target configuration") + } + attr = r.(*ociuploadattr.Attribute) + case []byte: + r, err := ociuploadattr.AttributeType{}.Decode(a, runtime.DefaultYAMLEncoding) + if err != nil { + return true, errors.Wrapf(err, "cannot unmarshal blob handler target configuration") + } + attr = r.(*ociuploadattr.Attribute) + default: + return true, fmt.Errorf("unexpected type %T for oci blob handler target", a) + } + + var mimes []string + opts := cpi.NewBlobHandlerOptions(olist...) + if opts.MimeType != "" { + found := false + for _, a := range artdesc.ArchiveBlobTypes() { + if a == opts.MimeType { + found = true + break + } + } + if !found { + return true, fmt.Errorf("unexpected type mime type %q for oci blob handler target", opts.MimeType) + } + mimes = append(mimes, opts.MimeType) + } else { + mimes = artdesc.ArchiveBlobTypes() + } + + h := NewArtefactHandler(attr) + for _, m := range mimes { + opts.MimeType = m + ctx.BlobHandlers().Register(h, opts) + } + + return true, nil +} diff --git a/pkg/contexts/ocm/blobhandler/generic/ocirepo/upload_test.go b/pkg/contexts/ocm/blobhandler/generic/ocirepo/upload_test.go index 92ebe4e86..17e89c3bc 100644 --- a/pkg/contexts/ocm/blobhandler/generic/ocirepo/upload_test.go +++ b/pkg/contexts/ocm/blobhandler/generic/ocirepo/upload_test.go @@ -5,11 +5,10 @@ package ocirepo_test import ( - "io" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/open-component-model/ocm/pkg/env/builder" + . "github.com/open-component-model/ocm/pkg/testutils" "github.com/open-component-model/ocm/pkg/common/accessio" "github.com/open-component-model/ocm/pkg/common/accessobj" @@ -20,6 +19,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/ociartefact" "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/ociuploadattr" v1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/comparch" ctfocm "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" "github.com/open-component-model/ocm/pkg/contexts/ocm/resourcetypes" @@ -36,11 +36,6 @@ const CTF = "ctf" const COPY = "ctf.copy" const TARGET = "/tmp/target" -func Close(closer io.Closer) { - err := closer.Close() - ExpectWithOffset(1, err).To(Succeed()) -} - const OCIHOST = "alias" const OCIPATH = "/tmp/source" const OCINAMESPACE = "ocm/value" @@ -53,8 +48,7 @@ var _ = Describe("upload", func() { env = NewBuilder(tenv.NewEnvironment()) // fake OCI registry - spec, err := ctfoci.NewRepositorySpec(accessobj.ACC_READONLY, OCIPATH, accessio.PathFileSystem(env.FileSystem())) - Expect(err).To(Succeed()) + spec := Must(ctfoci.NewRepositorySpec(accessobj.ACC_READONLY, OCIPATH, accessio.PathFileSystem(env.FileSystem()))) env.OCIContext().SetAlias(OCIHOST, spec) env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() { @@ -71,14 +65,6 @@ var _ = Describe("upload", func() { }) env.OCICommonTransport(TARGET, accessio.FormatDirectory) - }) - - AfterEach(func() { - env.Cleanup() - }) - - It("transfers oci artefact", func() { - ctx := env.OCMContext() env.ComponentArchive(CA, accessio.FormatDirectory, COMP, VERS, func() { env.Provider("mandelsoft") @@ -88,38 +74,41 @@ var _ = Describe("upload", func() { ) }) }) - ca, err := comparch.Open(ctx, accessobj.ACC_READONLY, CA, 0, env) - Expect(err).To(Succeed()) + + ca := Must(comparch.Open(env.OCMContext(), accessobj.ACC_READONLY, CA, 0, env)) oca := accessio.OnceCloser(ca) defer Close(oca) - ctf, err := ctfocm.Create(ctx, accessobj.ACC_CREATE, CTF, 0700, env) - Expect(err).To(Succeed()) + ctf := Must(ctfocm.Create(env.OCMContext(), accessobj.ACC_CREATE, CTF, 0700, env)) octf := accessio.OnceCloser(ctf) defer Close(octf) - handler, err := standard.New(standard.ResourcesByValue()) - Expect(err).To(Succeed()) + handler := Must(standard.New(standard.ResourcesByValue())) - err = transfer.TransferVersion(nil, nil, ca, ctf, handler) - Expect(err).To(Succeed()) - oca.Close() + MustBeSuccessful(transfer.TransferVersion(nil, nil, ca, ctf, handler)) // now we have a transport archive with local blob for the image + }) - cv, err := ctf.LookupComponentVersion(COMP, VERS) - Expect(err).To(Succeed()) + AfterEach(func() { + env.Cleanup() + }) + + It("transfers oci artefact", func() { + ctx := env.OCMContext() + + ctf := Must(ctfocm.Open(ctx, accessobj.ACC_READONLY, CTF, 0700, env)) + defer Close(ctf, "ctf") + + cv := Must(ctf.LookupComponentVersion(COMP, VERS)) ocv := accessio.OnceCloser(cv) defer Close(ocv) - ra, err := cv.GetResourceByIndex(0) - Expect(err).To(Succeed()) - acc, err := ra.Access() - Expect(err).To(Succeed()) + ra := Must(cv.GetResourceByIndex(0)) + acc := Must(ra.Access()) Expect(acc.GetKind()).To(Equal(localblob.Type)) // transfer component - copy, err := ctfocm.Create(ctx, accessobj.ACC_CREATE, COPY, 0700, env) - Expect(err).To(Succeed()) + copy := Must(ctfocm.Create(ctx, accessobj.ACC_CREATE, COPY, 0700, env)) ocopy := accessio.OnceCloser(copy) defer Close(ocopy) @@ -127,21 +116,16 @@ var _ = Describe("upload", func() { attr := ociuploadattr.New(TARGET + grammar.RepositorySeparator + grammar.RepositorySeparator + "copy") ociuploadattr.Set(ctx, attr) - err = transfer.TransferVersion(nil, nil, cv, copy, nil) - Expect(err).To(Succeed()) + MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, copy, nil)) // check type - cv2, err := copy.LookupComponentVersion(COMP, VERS) - Expect(err).To(Succeed()) + cv2 := Must(copy.LookupComponentVersion(COMP, VERS)) ocv2 := accessio.OnceCloser(cv2) defer Close(ocv2) - ra, err = cv2.GetResourceByIndex(0) - Expect(err).To(Succeed()) - acc, err = ra.Access() - Expect(err).To(Succeed()) + ra = Must(cv2.GetResourceByIndex(0)) + acc = Must(ra.Access()) Expect(acc.GetKind()).To(Equal(ociartefact.Type)) - val, err := ctx.AccessSpecForSpec(acc) - Expect(err).To(Succeed()) + val := Must(ctx.AccessSpecForSpec(acc)) // TODO: the result is invalid for ctf: better handling for ctf refs Expect(val.(*ociartefact.AccessSpec).ImageReference).To(Equal("/tmp/target//copy/ocm/value:v2.0")) @@ -150,6 +134,47 @@ var _ = Describe("upload", func() { Expect(err).To(Succeed()) defer Close(target) Expect(target.ExistsArtefact("copy/ocm/value", "v2.0")).To(BeTrue()) + }) + + It("transfers oci artefact with named handler", func() { + ctx := env.OCMContext() + + ctf := Must(ctfocm.Open(ctx, accessobj.ACC_READONLY, CTF, 0700, env)) + defer Close(ctf, "ctf") + + cv := Must(ctf.LookupComponentVersion(COMP, VERS)) + ocv := accessio.OnceCloser(cv) + defer Close(ocv) + ra := Must(cv.GetResourceByIndex(0)) + acc := Must(ra.Access()) + Expect(acc.GetKind()).To(Equal(localblob.Type)) + + // transfer component + copy := Must(ctfocm.Create(ctx, accessobj.ACC_CREATE, COPY, 0700, env)) + ocopy := accessio.OnceCloser(copy) + defer Close(ocopy) + // prepare upload to target OCI repo + attr := ociuploadattr.New(TARGET + grammar.RepositorySeparator + grammar.RepositorySeparator + "copy") + MustBeSuccessful(registration.RegisterBlobHandlerByName(ctx, "ocm/ociArtifacts", attr)) + + MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, copy, nil)) + + // check type + cv2 := Must(copy.LookupComponentVersion(COMP, VERS)) + ocv2 := accessio.OnceCloser(cv2) + defer Close(ocv2) + ra = Must(cv2.GetResourceByIndex(0)) + acc = Must(ra.Access()) + Expect(acc.GetKind()).To(Equal(ociartefact.Type)) + val := Must(ctx.AccessSpecForSpec(acc)) + // TODO: the result is invalid for ctf: better handling for ctf refs + Expect(val.(*ociartefact.AccessSpec).ImageReference).To(Equal("/tmp/target//copy/ocm/value:v2.0")) + + attr.Close() + target, err := ctfoci.Open(ctx.OCIContext(), accessobj.ACC_READONLY, TARGET, 0, env) + Expect(err).To(Succeed()) + defer Close(target) + Expect(target.ExistsArtefact("copy/ocm/value", "v2.0")).To(BeTrue()) }) }) diff --git a/pkg/contexts/ocm/blobhandler/generic/plugin/registration.go b/pkg/contexts/ocm/blobhandler/generic/plugin/registration.go new file mode 100644 index 000000000..0176ba341 --- /dev/null +++ b/pkg/contexts/ocm/blobhandler/generic/plugin/registration.go @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package plugin + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" + "github.com/open-component-model/ocm/pkg/errors" + "github.com/open-component-model/ocm/pkg/runtime" +) + +type Config = json.RawMessage + +func init() { + cpi.RegisterBlobHandlerRegistrationHandler("plugin", &RegistrationHandler{}) +} + +type RegistrationHandler struct{} + +var _ cpi.BlobHandlerRegistrationHandler = (*RegistrationHandler)(nil) + +func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config cpi.BlobHandlerConfig, olist ...cpi.BlobHandlerOption) (bool, error) { + path := cpi.NewNamePath(handler) + + if config == nil { + return true, fmt.Errorf("target specification required") + } + + if len(path) < 1 || len(path) > 2 { + return true, fmt.Errorf("plugin handler must be of the form [/]") + } + + opts := cpi.NewBlobHandlerOptions(olist...) + + var attr Config + switch a := config.(type) { + case json.RawMessage: + attr = a + case []byte: + err := runtime.DefaultYAMLEncoding.Unmarshal(a, &attr) + if err != nil { + return true, errors.Wrapf(err, "invalid target specification") + } + attr = a + default: + data, err := json.Marshal(config) + if err != nil { + return true, errors.Wrapf(err, "invalid target specification") + } + attr = data + } + + name := "" + if len(path) > 1 { + name = path[1] + } + _, _, err := RegisterBlobHandler(ctx, path[0], name, opts.ArtefactType, opts.MimeType, attr) + return true, err +} + +func RegisterBlobHandler(ctx ocm.Context, pname, name string, artType, mediaType string, target json.RawMessage) (string, plugin.UploaderKeySet, error) { + 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 != "" { + if p.GetUploaderDescriptor(name) == nil { + return "", nil, fmt.Errorf("uploader %s not found in plugin %q", name, pname) + } + } + keys := plugin.UploaderKeySet{}.Add(plugin.UploaderKey{}.SetArtefact(artType, mediaType)) + d := p.LookupUploader(name, artType, mediaType) + + if len(d) == 0 { + keys = p.LookupUploaderKeys(name, artType, mediaType) + if len(keys) == 0 { + if name == "" { + return "", nil, fmt.Errorf("no uploader found for [art:%q, media:%q]", artType, mediaType) + } + return "", nil, fmt.Errorf("uploader %s not valid for [art:%q, media:%q]", name, artType, mediaType) + } + d = p.LookupUploadersForKeys(name, keys) + } + if len(d) > 1 { + return "", nil, fmt.Errorf("multiple uploaders found for [art:%q, media:%q]: %s", artType, mediaType, strings.Join(d.GetNames(), ", ")) + } + h, err := New(p, d[0].Name, target) + if err != nil { + return d[0].Name, nil, err + } + for k := range keys { + ctx.BlobHandlers().Register(h, cpi.ForArtefactType(k.GetArtefactType()), cpi.ForMimeType(k.GetMediaType())) + } + return d[0].Name, keys, nil +} diff --git a/pkg/contexts/ocm/blobhandler/generic/plugin/upload_test.go b/pkg/contexts/ocm/blobhandler/generic/plugin/upload_test.go index 4307681a1..96451c3c4 100644 --- a/pkg/contexts/ocm/blobhandler/generic/plugin/upload_test.go +++ b/pkg/contexts/ocm/blobhandler/generic/plugin/upload_test.go @@ -25,9 +25,12 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm" "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugindirattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/blobhandler/generic/plugin" metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + plugin2 "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/config" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" + "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" "github.com/open-component-model/ocm/pkg/contexts/ocm/transfer" "github.com/open-component-model/ocm/pkg/contexts/ocm/transfer/transferhandler/standard" @@ -98,15 +101,7 @@ var _ = Describe("setup plugin cache", func() { Expect(p).NotTo(BeNil()) ctx.ConfigContext().ApplyConfig(config.New(PLUGIN, []byte(fmt.Sprintf(`{"root": "`+repodir+`"}`))), "plugin config") - registry.RegisterExtensions() - }) - - AfterEach(func() { - env.Cleanup() - os.RemoveAll(repodir) - }) - - It("uploads artefact", func() { + registration.RegisterExtensions(ctx) env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { env.Component(COMP, func() { @@ -120,18 +115,67 @@ var _ = Describe("setup plugin cache", func() { }) }) }) + }) + + AfterEach(func() { + env.Cleanup() + os.RemoveAll(repodir) + }) + + It("uploads artefact", func() { + repo := Must(ctf.Open(ctx, accessobj.ACC_READONLY, ARCH, 0, env)) + defer Close(repo, "source repo") + + cv := Must(repo.LookupComponentVersion(COMP, VERS)) + defer Close(cv, "source version") + + _, _, err := plugin.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", []byte("{}")) + MustFailWithMessage(err, + "plugin uploader test/testuploader: path missing in repository spec", + ) + repospec := Must(json.Marshal(repoSpec)) + name, keys, err := plugin.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", repospec) + MustBeSuccessful(err) + Expect(name).To(Equal("testuploader")) + Expect(keys).To(Equal(plugin2.UploaderKeySet{}.Add(plugin2.UploaderKey{}.SetArtefact(RSCTYPE, "")))) + + tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0700, accessio.FormatDirectory, env)) + defer Close(tgt, "target repo") + + MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, tgt, Must(standard.New(standard.ResourcesByValue())))) + Expect(env.DirExists(OUT)).To(BeTrue()) + + Expect(vfs.FileExists(osfs.New(), filepath.Join(repodir, REPO, HINT))).To(BeTrue()) + + tcv := Must(tgt.LookupComponentVersion(COMP, VERS)) + defer Close(tcv, "target version") + + r := Must(tcv.GetResourceByIndex(0)) + a := Must(r.Access()) + + var spec AccessSpec + MustBeSuccessful(json.Unmarshal(Must(json.Marshal(a)), &spec)) + Expect(spec).To(Equal(*accessSpec)) + + m := Must(a.AccessMethod(tcv)) + defer Close(m, "method") + + Expect(string(Must(m.Get()))).To(Equal(CONTENT)) + }) + It("uploads after abstract registration", func() { repo := Must(ctf.Open(ctx, accessobj.ACC_READONLY, ARCH, 0, env)) defer Close(repo, "source repo") cv := Must(repo.LookupComponentVersion(COMP, VERS)) defer Close(cv, "source version") - MustFailWithMessage(plugincacheattr.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", []byte("{}")), - "plugin test: path missing in repository spec", + MustFailWithMessage(registration.RegisterBlobHandlerByName(ctx, "plugin/test", []byte("{}"), registration.ForArtefactType(RSCTYPE)), + //MustFailWithMessage(plugin.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", []byte("{}")), + "plugin uploader test/testuploader: path missing in repository spec", ) repospec := Must(json.Marshal(repoSpec)) - MustBeSuccessful(plugincacheattr.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", repospec)) + MustBeSuccessful(registration.RegisterBlobHandlerByName(ctx, "plugin/test", repospec)) tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0700, accessio.FormatDirectory, env)) defer Close(tgt, "target repo") diff --git a/pkg/contexts/ocm/compdesc/meta/v1/identity.go b/pkg/contexts/ocm/compdesc/meta/v1/identity.go index 97d5fa9fa..b892c396c 100644 --- a/pkg/contexts/ocm/compdesc/meta/v1/identity.go +++ b/pkg/contexts/ocm/compdesc/meta/v1/identity.go @@ -66,6 +66,13 @@ func (i *Identity) Set(name, value string) { } } +func (i Identity) Get(name string) string { + if i != nil { + return i[name] + } + return "" +} + func (i Identity) Remove(name string) bool { if i != nil { delete(i, name) diff --git a/pkg/contexts/ocm/consts/resource_types.go b/pkg/contexts/ocm/consts/resource_types.go index ae35d6e51..4360d2858 100644 --- a/pkg/contexts/ocm/consts/resource_types.go +++ b/pkg/contexts/ocm/consts/resource_types.go @@ -20,4 +20,8 @@ const ( Blob = resourcetypes.BLOB // FileSystem describes a directory structure stored as archive (tar, tgz). FileSystem = resourcetypes.FILESYSTEM + // Executable describes an OS executable. + Executable = resourcetypes.EXECUTABLE + // OCMPlugin describes an OS executable OCM plugin. + OCMPlugin = resourcetypes.OCM_PLUGIN ) diff --git a/pkg/contexts/ocm/cpi/interface.go b/pkg/contexts/ocm/cpi/interface.go index 9681216b1..9c38afc32 100644 --- a/pkg/contexts/ocm/cpi/interface.go +++ b/pkg/contexts/ocm/cpi/interface.go @@ -33,6 +33,7 @@ type ( ContextProvider = internal.ContextProvider ComponentVersionResolver = internal.ComponentVersionResolver Repository = internal.Repository + RepositoryTypeScheme = internal.RepositoryTypeScheme RepositorySpecHandlers = internal.RepositorySpecHandlers RepositorySpecHandler = internal.RepositorySpecHandler UniformRepositorySpec = internal.UniformRepositorySpec @@ -60,8 +61,13 @@ type ( type ( BlobHandler = internal.BlobHandler BlobHandlerOption = internal.BlobHandlerOption + BlobHandlerOptions = internal.BlobHandlerOptions + BlobHandlerRegistry = internal.BlobHandlerRegistry StorageContext = internal.StorageContext ImplementationRepositoryType = internal.ImplementationRepositoryType + + BlobHandlerConfig = internal.BlobHandlerConfig + BlobHandlerRegistrationHandler = internal.BlobHandlerRegistrationHandler ) type ( @@ -71,6 +77,16 @@ type ( DigestDescriptor = internal.DigestDescriptor ) +type NamePath = internal.NamePath + +func NewNamePath(p string) NamePath { + return internal.NewNamePath(p) +} + +func NewBlobHandlerOptions(olist ...BlobHandlerOption) *BlobHandlerOptions { + return internal.NewBlobHandlerOptions(olist...) +} + func New() Context { return internal.Builder{}.New() } @@ -111,6 +127,10 @@ func RegisterBlobHandler(handler BlobHandler, opts ...BlobHandlerOption) { internal.RegisterBlobHandler(handler, opts...) } +func RegisterBlobHandlerRegistrationHandler(path string, handler BlobHandlerRegistrationHandler) { + internal.RegisterBlobHandlerRegistrationHandler(path, handler) +} + func MustRegisterDigester(digester BlobDigester, arttypes ...string) { internal.MustRegisterDigester(digester, arttypes...) } diff --git a/pkg/contexts/ocm/download/handlers/blob/handler.go b/pkg/contexts/ocm/download/handlers/blob/handler.go index d9878017b..78595ce55 100644 --- a/pkg/contexts/ocm/download/handlers/blob/handler.go +++ b/pkg/contexts/ocm/download/handlers/blob/handler.go @@ -9,10 +9,10 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/open-component-model/ocm/pkg/common" "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" "github.com/open-component-model/ocm/pkg/contexts/ocm/download" "github.com/open-component-model/ocm/pkg/errors" - "github.com/open-component-model/ocm/pkg/out" ) type Handler struct{} @@ -29,7 +29,7 @@ func wrapErr(err error, racc cpi.ResourceAccess) error { return errors.Wrapf(err, "resource %s/%s%s", m.GetName(), m.GetVersion(), m.ExtraIdentity.String()) } -func (_ Handler) Download(ctx out.Context, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { +func (_ Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { rd, err := cpi.ResourceReader(racc) if err != nil { return true, "", wrapErr(err, racc) @@ -42,7 +42,7 @@ func (_ Handler) Download(ctx out.Context, racc cpi.ResourceAccess, path string, defer file.Close() n, err := io.Copy(file, rd) if err == nil { - out.Outf(ctx, "%s: %d byte(s) written\n", path, n) + p.Printf("%s: %d byte(s) written\n", path, n) } return true, path, wrapErr(err, racc) } diff --git a/pkg/contexts/ocm/download/handlers/executable/handler.go b/pkg/contexts/ocm/download/handlers/executable/handler.go new file mode 100644 index 000000000..7d4775fad --- /dev/null +++ b/pkg/contexts/ocm/download/handlers/executable/handler.go @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package blob + +import ( + "io" + + "github.com/mandelsoft/vfs/pkg/vfs" + + "github.com/open-component-model/ocm/pkg/common" + "github.com/open-component-model/ocm/pkg/common/compression" + "github.com/open-component-model/ocm/pkg/contexts/ocm/consts" + "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/download" + "github.com/open-component-model/ocm/pkg/errors" + "github.com/open-component-model/ocm/pkg/mime" +) + +type Handler struct{} + +func init() { + download.Register(consts.OCMPlugin, mime.MIME_OCTET, &Handler{}) + download.Register(consts.OCMPlugin, mime.MIME_GZIP, &Handler{}) + download.Register(consts.Executable, mime.MIME_OCTET, &Handler{}) + download.Register(consts.Executable, mime.MIME_GZIP, &Handler{}) +} + +func wrapErr(err error, racc cpi.ResourceAccess) error { + if err == nil { + return nil + } + m := racc.Meta() + return errors.Wrapf(err, "resource %s/%s%s", m.GetName(), m.GetVersion(), m.ExtraIdentity.String()) +} + +func (_ Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { + rd, err := cpi.ResourceReader(racc) + if err != nil { + return true, "", wrapErr(err, racc) + } + defer rd.Close() + + r, _, err := compression.AutoDecompress(rd) + if err != nil { + return true, "", err + } + file, err := fs.OpenFile(path, vfs.O_TRUNC|vfs.O_CREATE|vfs.O_WRONLY, 0o660) + if err != nil { + return true, "", wrapErr(errors.Wrapf(err, "creating target file %q", path), racc) + } + n, err := io.Copy(file, r) + file.Close() + if err == nil { + p.Printf("%s: %d byte(s) written\n", path, n) + fs.Chmod(path, 0o755) + } else { + fs.Remove(path) + } + return true, path, wrapErr(err, racc) +} diff --git a/pkg/contexts/ocm/download/handlers/helm/handler.go b/pkg/contexts/ocm/download/handlers/helm/handler.go index 3362eef6e..4e9b3a27c 100644 --- a/pkg/contexts/ocm/download/handlers/helm/handler.go +++ b/pkg/contexts/ocm/download/handlers/helm/handler.go @@ -10,6 +10,7 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/open-component-model/ocm/pkg/common" "github.com/open-component-model/ocm/pkg/common/accessio" "github.com/open-component-model/ocm/pkg/common/accessobj" "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" @@ -19,7 +20,6 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/download" "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/mime" - "github.com/open-component-model/ocm/pkg/out" ) const TYPE = consts.HelmChart @@ -30,7 +30,7 @@ func init() { download.RegisterForArtefactType(TYPE, &Handler{}) } -func (h Handler) Download(ctx out.Context, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { +func (h Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { meth, err := racc.AccessMethod() if err != nil { return false, "", err @@ -66,7 +66,7 @@ func (h Handler) Download(ctx out.Context, racc cpi.ResourceAccess, path string, if err != nil { return true, "", err } - err = h.write(ctx, blob, path, fs) + err = h.write(p, blob, path, fs) if err != nil { return true, "", err } @@ -76,7 +76,7 @@ func (h Handler) Download(ctx out.Context, racc cpi.ResourceAccess, path string, if err != nil { return true, "", err } - err = h.write(ctx, blob, path, fs) + err = h.write(p, blob, path, fs) if err != nil { return true, "", err } @@ -84,7 +84,7 @@ func (h Handler) Download(ctx out.Context, racc cpi.ResourceAccess, path string, return true, path, nil } -func (_ Handler) write(ctx out.Context, blob accessio.BlobAccess, path string, fs vfs.FileSystem) error { +func (_ Handler) write(p common.Printer, blob accessio.BlobAccess, path string, fs vfs.FileSystem) error { cr, err := blob.Reader() if err != nil { return err @@ -97,7 +97,7 @@ func (_ Handler) write(ctx out.Context, blob accessio.BlobAccess, path string, f defer file.Close() n, err := io.Copy(file, cr) if err == nil { - out.Outf(ctx, "%s: %d byte(s) written\n", path, n) + p.Printf("%s: %d byte(s) written\n", path, n) } return nil } diff --git a/pkg/contexts/ocm/download/handlers/init.go b/pkg/contexts/ocm/download/handlers/init.go index aef5e03a8..c53c1939a 100644 --- a/pkg/contexts/ocm/download/handlers/init.go +++ b/pkg/contexts/ocm/download/handlers/init.go @@ -6,5 +6,6 @@ package handlers import ( _ "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/blob" + _ "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/executable" _ "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/helm" ) diff --git a/pkg/contexts/ocm/download/handlers/plugin/download_test.go b/pkg/contexts/ocm/download/handlers/plugin/download_test.go index 988f4f934..573624f5b 100644 --- a/pkg/contexts/ocm/download/handlers/plugin/download_test.go +++ b/pkg/contexts/ocm/download/handlers/plugin/download_test.go @@ -16,6 +16,7 @@ import ( . "github.com/open-component-model/ocm/pkg/env/builder" . "github.com/open-component-model/ocm/pkg/testutils" + "github.com/open-component-model/ocm/pkg/common" "github.com/open-component-model/ocm/pkg/common/accessio" "github.com/open-component-model/ocm/pkg/common/accessobj" "github.com/open-component-model/ocm/pkg/contexts/ocm" @@ -23,6 +24,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugindirattr" metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" "github.com/open-component-model/ocm/pkg/contexts/ocm/download" + "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/plugin" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/config" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" @@ -103,17 +105,17 @@ var _ = Describe("setup plugin cache", func() { cv := Must(repo.LookupComponentVersion(COMP, VERS)) defer Close(cv, "source version") - MustFailWithMessage(plugincacheattr.RegisterDownloadHandler(env.OCMContext(), "test", "", "blah", ""), + MustFailWithMessage(plugin.RegisterDownloadHandler(env.OCMContext(), "test", "", "blah", ""), "no downloader found for [art:\"blah\", media:\"\"]", ) - MustBeSuccessful(plugincacheattr.RegisterDownloadHandler(env.OCMContext(), "test", "", RSCTYPE, "")) + MustBeSuccessful(plugin.RegisterDownloadHandler(env.OCMContext(), "test", "", RSCTYPE, "")) racc := Must(cv.GetResourceByIndex(0)) file := filepath.Join(repodir, "download") octx, buf := out.NewBuffered() - ok, eff, err := download.For(env.OCMContext()).Download(octx, racc, file, nil) + ok, eff, err := download.For(env.OCMContext()).Download(common.NewPrinter(octx.StdOut()), racc, file, nil) MustBeSuccessful(err) Expect(buf.String()).To(Equal("")) diff --git a/pkg/contexts/ocm/download/handlers/plugin/handler.go b/pkg/contexts/ocm/download/handlers/plugin/handler.go index dbc004e16..583f4d29a 100644 --- a/pkg/contexts/ocm/download/handlers/plugin/handler.go +++ b/pkg/contexts/ocm/download/handlers/plugin/handler.go @@ -7,13 +7,13 @@ package plugin import ( "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/open-component-model/ocm/pkg/common" "github.com/open-component-model/ocm/pkg/common/accessio" "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" "github.com/open-component-model/ocm/pkg/contexts/ocm/download" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" "github.com/open-component-model/ocm/pkg/errors" - "github.com/open-component-model/ocm/pkg/out" ) // pluginHandler stores artefact blobs as OCIArtefacts. @@ -34,7 +34,7 @@ func New(p plugin.Plugin, name string) (download.Handler, error) { }, nil } -func (b *pluginHandler) Download(ctx out.Context, racc cpi.ResourceAccess, path string, _ vfs.FileSystem) (bool, string, error) { +func (b *pluginHandler) Download(_ common.Printer, racc cpi.ResourceAccess, path string, _ vfs.FileSystem) (bool, string, error) { m, err := racc.AccessMethod() if err != nil { return true, "", err diff --git a/pkg/contexts/ocm/download/handlers/plugin/registration.go b/pkg/contexts/ocm/download/handlers/plugin/registration.go new file mode 100644 index 000000000..c19eaf2dc --- /dev/null +++ b/pkg/contexts/ocm/download/handlers/plugin/registration.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package plugin + +import ( + "fmt" + + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/download" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" + "github.com/open-component-model/ocm/pkg/errors" +) + +func RegisterDownloadHandler(ctx ocm.Context, pname, name string, artType, mediaType string) error { + set := plugincacheattr.Get(ctx) + if set == nil { + return errors.ErrUnknown(plugin.KIND_PLUGIN, pname) + } + + p := set.Get(pname) + if p == nil { + return errors.ErrUnknown(plugin.KIND_PLUGIN, pname) + } + d := p.LookupDownloader(name, artType, mediaType) + if len(d) == 0 { + if name == "" { + return fmt.Errorf("no downloader found for [art:%q, media:%q]", artType, mediaType) + } + return fmt.Errorf("downloader %s not valid for [art:%q, media:%q]", name, artType, mediaType) + } + for _, e := range d { + h, err := New(p, e.Name) + if err != nil { + return err + } + download.For(ctx).Register(artType, mediaType, h) + } + return nil +} diff --git a/pkg/contexts/ocm/download/registry.go b/pkg/contexts/ocm/download/registry.go index 9dbf476e8..7d2938b4f 100644 --- a/pkg/contexts/ocm/download/registry.go +++ b/pkg/contexts/ocm/download/registry.go @@ -9,23 +9,23 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/open-component-model/ocm/pkg/common" "github.com/open-component-model/ocm/pkg/contexts/datacontext" "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" "github.com/open-component-model/ocm/pkg/contexts/ocm/utils/registry" "github.com/open-component-model/ocm/pkg/errors" - "github.com/open-component-model/ocm/pkg/out" ) const ALL = "*" type Handler interface { - Download(ctx out.Context, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) + Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) } type Registry interface { Register(arttype, mediatype string, hdlr Handler) Handler - DownloadAsBlob(ctx out.Context, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) + DownloadAsBlob(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) } type _registry struct { @@ -52,7 +52,7 @@ func (r *_registry) getHandlers(arttype, mediatype string) []Handler { return r.handlers.LookupHandler(registry.RegistrationKey{arttype, mediatype}) } -func (r *_registry) Download(ctx out.Context, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { +func (r *_registry) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { art := racc.Meta().GetType() m, err := racc.AccessMethod() if err != nil { @@ -60,20 +60,20 @@ func (r *_registry) Download(ctx out.Context, racc cpi.ResourceAccess, path stri } defer m.Close() mime := m.MimeType() - if ok, p, err := r.download(r.getHandlers(art, mime), ctx, racc, path, fs); ok { + if ok, p, err := r.download(r.getHandlers(art, mime), p, racc, path, fs); ok { return ok, p, err } - return r.download(r.getHandlers(ALL, ""), ctx, racc, path, fs) + return r.download(r.getHandlers(ALL, ""), p, racc, path, fs) } -func (r *_registry) DownloadAsBlob(ctx out.Context, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { - return r.download(r.getHandlers(ALL, ""), ctx, racc, path, fs) +func (r *_registry) DownloadAsBlob(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { + return r.download(r.getHandlers(ALL, ""), p, racc, path, fs) } -func (r *_registry) download(list []Handler, ctx out.Context, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { +func (r *_registry) download(list []Handler, p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { errs := errors.ErrListf("download") for _, h := range list { - ok, p, err := h.Download(ctx, racc, path, fs) + ok, p, err := h.Download(p, racc, path, fs) if ok { return ok, p, err } diff --git a/pkg/contexts/ocm/internal/blobhandler.go b/pkg/contexts/ocm/internal/blobhandler.go index 5bdc039c8..f5aa3c994 100644 --- a/pkg/contexts/ocm/internal/blobhandler.go +++ b/pkg/contexts/ocm/internal/blobhandler.go @@ -10,6 +10,7 @@ import ( "strings" "sync" + "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/utils" ) @@ -99,6 +100,21 @@ type BlobHandlerOptions struct { Priority int } +func NewBlobHandlerOptions(olist ...BlobHandlerOption) *BlobHandlerOptions { + var opts BlobHandlerOptions + for _, o := range olist { + o.ApplyBlobHandlerOptionTo(&opts) + } + return &opts +} + +func (o BlobHandlerOptions) ApplyBlobHandlerOptionTo(opts *BlobHandlerOptions) { + if o.Priority > 0 { + opts.Priority = o.Priority + } + o.BlobHandlerKey.ApplyBlobHandlerOptionTo(opts) +} + type BlobHandlerOption interface { ApplyBlobHandlerOptionTo(*BlobHandlerOptions) } @@ -166,8 +182,147 @@ func ForArtefactType(artefacttype string) BlobHandlerOption { //////////////////////////////////////////////////////////////////////////////// +type BlobHandlerConfig interface{} + +type BlobHandlerRegistrationHandler interface { + RegisterByName(handler string, ctx Context, config BlobHandlerConfig, opts ...BlobHandlerOption) (bool, error) +} + +type BlobHandlerRegistrationRegistry interface { + BlobHandlerRegistrationHandler + RegisterRegistrationHandler(path string, handler BlobHandlerRegistrationHandler) + GetRegistrationHandlers(name string) []*RegistrationHandlerInfo +} + +type NamePath []string + +func NewNamePath(path string) NamePath { + return strings.Split(path, "/") +} + +func (p NamePath) Compare(o NamePath) int { + if d := len(p) - len(o); d != 0 { + return d + } + for i, e := range p { + if d := strings.Compare(e, o[i]); d != 0 { + return d + } + } + return 0 +} + +func (p NamePath) IsPrefixOf(o NamePath) bool { + if len(p) > len(o) { + return false + } + for i, e := range p { + if e != o[i] { + return false + } + } + return true +} + +type RegistrationHandlerInfo struct { + prefix NamePath + handler BlobHandlerRegistrationHandler +} + +func NewRegistrationHandlerInfo(path string, handler BlobHandlerRegistrationHandler) *RegistrationHandlerInfo { + return &RegistrationHandlerInfo{ + prefix: NewNamePath(path), + handler: handler, + } +} + +func (i *RegistrationHandlerInfo) RegisterByName(handler string, ctx Context, config BlobHandlerConfig, opts ...BlobHandlerOption) (bool, error) { + path := NewNamePath(handler) + + if !i.prefix.IsPrefixOf(path) { + return false, nil + } + return i.handler.RegisterByName(strings.Join(path[len(i.prefix):], "/"), ctx, config, opts...) +} + +type handlerRegistrationRegistry struct { + lock sync.RWMutex + base BlobHandlerRegistrationRegistry + handlers []*RegistrationHandlerInfo +} + +func NewBlobHandlerRegistrationRegistry(base ...BlobHandlerRegistrationRegistry) BlobHandlerRegistrationRegistry { + return &handlerRegistrationRegistry{base: utils.Optional(base...)} +} + +func (c *handlerRegistrationRegistry) RegisterRegistrationHandler(path string, handler BlobHandlerRegistrationHandler) { + c.lock.Lock() + defer c.lock.Unlock() + + comps := strings.Split(path, "/") + n := &RegistrationHandlerInfo{ + prefix: comps, + handler: handler, + } + + var i int + var h *RegistrationHandlerInfo + for i, h = range c.handlers { + if h.prefix.Compare(comps) < 0 { + break + } + } + c.handlers = append(c.handlers[:i], append([]*RegistrationHandlerInfo{n}, c.handlers[i:]...)...) +} + +func (c *handlerRegistrationRegistry) GetRegistrationHandlers(name string) []*RegistrationHandlerInfo { + c.lock.RLock() + defer c.lock.RUnlock() + + var result []*RegistrationHandlerInfo + path := NewNamePath(name) + for _, h := range c.handlers { + if h.prefix.IsPrefixOf(path) { + result = append(result, h) + } + } + + if c.base != nil { + base := c.base.GetRegistrationHandlers(name) + i := 0 + for _, h := range base { + for i != len(result) && result[i].prefix.Compare(h.prefix) >= 0 { + i++ + } + result = append(result[:i], append([]*RegistrationHandlerInfo{h}, result[i:]...)...) + i++ + } + } + return result +} + +func (c *handlerRegistrationRegistry) RegisterByName(handler string, ctx Context, config BlobHandlerConfig, opts ...BlobHandlerOption) (bool, error) { + list := c.GetRegistrationHandlers(handler) + errlist := errors.ErrListf("blob handler registration") + for _, h := range list { + ok, err := h.RegisterByName(handler, ctx, config, opts...) + if ok { + return ok, err + } + errlist.Add(err) + } + if errlist.Len() > 0 { + return false, errlist.Result() + } + return false, fmt.Errorf("no registration handler found for %s", handler) +} + +//////////////////////////////////////////////////////////////////////////////// + // BlobHandlerRegistry registers blob handlers to use in a dedicated ocm context. type BlobHandlerRegistry interface { + BlobHandlerRegistrationRegistry + IsInitial() bool // Copy provides a new independend copy of the registry. @@ -220,18 +375,29 @@ func (c *handlerCache) set(key BlobHandlerKey, h BlobHandler) { c.cache[key] = h } +type registrationHandlers = BlobHandlerRegistrationRegistry + type blobHandlerRegistry struct { lock sync.RWMutex base BlobHandlerRegistry handlers map[BlobHandlerKey]BlobHandler defhandler MultiBlobHandler - cache *handlerCache + + registrationHandlers + + cache *handlerCache } var DefaultBlobHandlerRegistry = NewBlobHandlerRegistry() func NewBlobHandlerRegistry(base ...BlobHandlerRegistry) BlobHandlerRegistry { - return &blobHandlerRegistry{handlers: map[BlobHandlerKey]BlobHandler{}, cache: newHandlerCache(), base: utils.Optional(base...)} + b := utils.Optional(base...) + return &blobHandlerRegistry{ + base: b, + handlers: map[BlobHandlerKey]BlobHandler{}, + registrationHandlers: NewBlobHandlerRegistrationRegistry(b), + cache: newHandlerCache(), + } } func (r *blobHandlerRegistry) Copy() BlobHandlerRegistry { @@ -253,10 +419,7 @@ func (r *blobHandlerRegistry) IsInitial() bool { } func (r *blobHandlerRegistry) Register(handler BlobHandler, olist ...BlobHandlerOption) BlobHandlerRegistry { - opts := &BlobHandlerOptions{} - for _, o := range olist { - o.ApplyBlobHandlerOptionTo(opts) - } + opts := NewBlobHandlerOptions(olist...) r.lock.Lock() defer r.lock.Unlock() @@ -386,3 +549,7 @@ func RegisterBlobHandler(handler BlobHandler, opts ...BlobHandlerOption) { func MustRegisterBlobHandler(handler BlobHandler, opts ...BlobHandlerOption) { DefaultBlobHandlerRegistry.Register(handler, opts...) } + +func RegisterBlobHandlerRegistrationHandler(path string, handler BlobHandlerRegistrationHandler) { + DefaultBlobHandlerRegistry.RegisterRegistrationHandler(path, handler) +} diff --git a/pkg/contexts/ocm/internal/blobhandler_test.go b/pkg/contexts/ocm/internal/blobhandler_test.go index 41e7a2a6b..d3d192053 100644 --- a/pkg/contexts/ocm/internal/blobhandler_test.go +++ b/pkg/contexts/ocm/internal/blobhandler_test.go @@ -9,6 +9,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/testutils" "github.com/open-component-model/ocm/pkg/contexts/ocm/internal" "github.com/open-component-model/ocm/pkg/mime" @@ -31,102 +32,213 @@ func (b BlobHandler) StoreBlob(blob internal.BlobAccess, artType string, hint st return nil, fmt.Errorf(b.name) } -var _ = Describe("blob handler registry test", func() { - var reg internal.BlobHandlerRegistry - var ext internal.BlobHandlerRegistry +type TestRegistrationHandler struct { + name string + registered map[string]interface{} +} - BeforeEach(func() { - reg = internal.NewBlobHandlerRegistry() - ext = internal.NewBlobHandlerRegistry(reg) - }) +func NewTestRegistrationHandler(name string) *TestRegistrationHandler { + return &TestRegistrationHandler{ + name: name, + registered: map[string]interface{}{}, + } +} - DescribeTable("priortizes complete specs", - func(eff *internal.BlobHandlerRegistry) { - reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) - reg.Register(&BlobHandler{"art"}, internal.ForArtefactType(ART)) - reg.Register(&BlobHandler{"all"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForArtefactType(ART), internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(&BlobHandler{"repomime"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) - - h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) - Expect(h).NotTo(BeNil()) - _, err := h.StoreBlob(nil, "", "", nil, nil) - Expect(err).To(MatchError(fmt.Errorf("all"))) - }, - Entry("plain", ®), - Entry("extended", &ext), - ) - - DescribeTable("priortizes mime", - func(eff *internal.BlobHandlerRegistry) { - reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) - reg.Register(&BlobHandler{"repomime"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) - - h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) - Expect(h).NotTo(BeNil()) - _, err := h.StoreBlob(nil, "", "", nil, nil) - Expect(err).To(MatchError(fmt.Errorf("repomime"))) - }, - Entry("plain", ®), - Entry("extended", &ext), - ) - - DescribeTable("priortizes mime", - func(eff *internal.BlobHandlerRegistry) { - reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) - reg.Register(&BlobHandler{"repomine"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(&BlobHandler{"repoart"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForArtefactType(ART)) - - h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) - Expect(h).NotTo(BeNil()) - _, err := h.StoreBlob(nil, "", "", nil, nil) - Expect(err).To(MatchError(fmt.Errorf("repoart"))) - }, - Entry("plain", ®), - Entry("extended", &ext), - ) - - DescribeTable("priortizes prio", - func(eff *internal.BlobHandlerRegistry) { - reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) - reg.Register(&BlobHandler{"all"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(&BlobHandler{"high"}, internal.WithPrio(internal.DEFAULT_BLOBHANDLER_PRIO+1)) - - h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) - Expect(h).NotTo(BeNil()) - _, err := h.StoreBlob(nil, "", "", nil, nil) - Expect(err).To(MatchError(fmt.Errorf("high"))) - }, - Entry("plain", ®), - Entry("extended", &ext), - ) - - DescribeTable("copies registries", - func(eff *internal.BlobHandlerRegistry) { - mine := &BlobHandler{"mine"} - repo := &BlobHandler{"repo"} - reg.Register(mine, internal.ForMimeType(mime.MIME_TEXT)) - reg.Register(repo, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) - - h := (*eff).LookupHandler(internal.ImplementationRepositoryType{internal.CONTEXT_TYPE, REPO}, ART, mime.MIME_OCTET) - Expect(h).To(Equal(internal.MultiBlobHandler{repo})) - - copy := (*eff).Copy() - new := &BlobHandler{"repo2"} - copy.Register(new, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) - - h = (*eff).LookupHandler(internal.ImplementationRepositoryType{internal.CONTEXT_TYPE, REPO}, ART, mime.MIME_OCTET) - Expect(h).To(Equal(internal.MultiBlobHandler{repo})) - - h = copy.LookupHandler(internal.ImplementationRepositoryType{internal.CONTEXT_TYPE, REPO}, ART, mime.MIME_OCTET) - Expect(h).To(Equal(internal.MultiBlobHandler{new})) - - }, - Entry("plain", ®), - Entry("extended", &ext), - ) +func (t *TestRegistrationHandler) RegisterByName(handler string, ctx internal.Context, config internal.BlobHandlerConfig, opts ...internal.BlobHandlerOption) (bool, error) { + path := internal.NewNamePath(handler) + if len(path) < 1 || path[0] != "match" { + return false, nil + } + t.registered[handler] = nil + return true, nil +} +var _ = Describe("blob handler registry test", func() { + Context("registration registry", func() { + var reg internal.BlobHandlerRegistrationRegistry + + var ha *TestRegistrationHandler + var hab *TestRegistrationHandler + var habc *TestRegistrationHandler + var habd *TestRegistrationHandler + var habe *TestRegistrationHandler + var hb *TestRegistrationHandler + + BeforeEach(func() { + reg = internal.NewBlobHandlerRegistrationRegistry() + ha = NewTestRegistrationHandler("a") + hab = NewTestRegistrationHandler("a/b") + habc = NewTestRegistrationHandler("a/b/c") + habd = NewTestRegistrationHandler("a/b/d") + habe = NewTestRegistrationHandler("a/b/e") + hb = NewTestRegistrationHandler("b") + }) + + It("registers ordered prefixes", func() { + reg.RegisterRegistrationHandler("a", ha) + reg.RegisterRegistrationHandler("a/b/d", habd) + reg.RegisterRegistrationHandler("a/b/c", habc) + reg.RegisterRegistrationHandler("a/b/e", habe) + reg.RegisterRegistrationHandler("a/b", hab) + reg.RegisterRegistrationHandler("b", hb) + + Expect(reg.GetRegistrationHandlers("a/b/c")).To(Equal([]*internal.RegistrationHandlerInfo{ + internal.NewRegistrationHandlerInfo("a/b/c", habc), + internal.NewRegistrationHandlerInfo("a/b", hab), + internal.NewRegistrationHandlerInfo("a", ha), + })) + }) + + It("registers ordered prefixes", func() { + reg.RegisterRegistrationHandler("a", ha) + reg.RegisterRegistrationHandler("a/b/d", habd) + reg.RegisterRegistrationHandler("a/b/c", habc) + reg.RegisterRegistrationHandler("a/b/e", habe) + reg.RegisterRegistrationHandler("a/b", hab) + reg.RegisterRegistrationHandler("b", hb) + + _, err := reg.RegisterByName("a/b/c/d", nil, nil) + MustFailWithMessage(err, "no registration handler found for a/b/c/d") + Expect(Must(reg.RegisterByName("a/b/c/match/d", nil, nil))).To(BeTrue()) + Expect(Must(reg.RegisterByName("a/b/c/match", nil, nil))).To(BeTrue()) + + Expect(ha.registered).To(Equal(map[string]interface{}{})) + Expect(hb.registered).To(Equal(map[string]interface{}{})) + Expect(hab.registered).To(Equal(map[string]interface{}{})) + Expect(habd.registered).To(Equal(map[string]interface{}{})) + Expect(habe.registered).To(Equal(map[string]interface{}{})) + Expect(habc.registered).To(Equal(map[string]interface{}{"match/d": nil, "match": nil})) + }) + + It("registers ordered prefixes", func() { + reg.RegisterRegistrationHandler("a/b/d", habd) + reg.RegisterRegistrationHandler("a/b/c", habc) + reg.RegisterRegistrationHandler("a/b/e", habe) + reg.RegisterRegistrationHandler("a", ha) + + derived := internal.NewBlobHandlerRegistrationRegistry(reg) + derived.RegisterRegistrationHandler("a/b", hab) + derived.RegisterRegistrationHandler("b", hb) + + list := derived.GetRegistrationHandlers("a/b/e") + Expect(list).To(Equal([]*internal.RegistrationHandlerInfo{ + internal.NewRegistrationHandlerInfo("a/b/e", habe), + internal.NewRegistrationHandlerInfo("a/b", hab), + internal.NewRegistrationHandlerInfo("a", ha), + })) + + _, err := reg.RegisterByName("a/b/e/d", nil, nil) + MustFailWithMessage(err, "no registration handler found for a/b/e/d") + Expect(Must(reg.RegisterByName("a/b/e/match/d", nil, nil))).To(BeTrue()) + Expect(Must(reg.RegisterByName("a/b/e/match", nil, nil))).To(BeTrue()) + + Expect(ha.registered).To(Equal(map[string]interface{}{})) + Expect(hb.registered).To(Equal(map[string]interface{}{})) + Expect(hab.registered).To(Equal(map[string]interface{}{})) + Expect(habd.registered).To(Equal(map[string]interface{}{})) + Expect(habc.registered).To(Equal(map[string]interface{}{})) + Expect(habe.registered).To(Equal(map[string]interface{}{"match/d": nil, "match": nil})) + }) + }) + + //////////////////////////////////////////////////////////////////////////// + + Context("blob handler registry", func() { + var reg internal.BlobHandlerRegistry + var ext internal.BlobHandlerRegistry + + BeforeEach(func() { + reg = internal.NewBlobHandlerRegistry() + ext = internal.NewBlobHandlerRegistry(reg) + }) + + DescribeTable("priortizes complete specs", + func(eff *internal.BlobHandlerRegistry) { + reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) + reg.Register(&BlobHandler{"art"}, internal.ForArtefactType(ART)) + reg.Register(&BlobHandler{"all"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForArtefactType(ART), internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(&BlobHandler{"repomime"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) + + h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) + Expect(h).NotTo(BeNil()) + _, err := h.StoreBlob(nil, "", "", nil, nil) + Expect(err).To(MatchError(fmt.Errorf("all"))) + }, + Entry("plain", ®), + Entry("extended", &ext), + ) + + DescribeTable("priortizes mime", + func(eff *internal.BlobHandlerRegistry) { + reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) + reg.Register(&BlobHandler{"repomime"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) + + h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) + Expect(h).NotTo(BeNil()) + _, err := h.StoreBlob(nil, "", "", nil, nil) + Expect(err).To(MatchError(fmt.Errorf("repomime"))) + }, + Entry("plain", ®), + Entry("extended", &ext), + ) + + DescribeTable("priortizes mime", + func(eff *internal.BlobHandlerRegistry) { + reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) + reg.Register(&BlobHandler{"repomine"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(&BlobHandler{"repoart"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForArtefactType(ART)) + + h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) + Expect(h).NotTo(BeNil()) + _, err := h.StoreBlob(nil, "", "", nil, nil) + Expect(err).To(MatchError(fmt.Errorf("repoart"))) + }, + Entry("plain", ®), + Entry("extended", &ext), + ) + + DescribeTable("priortizes prio", + func(eff *internal.BlobHandlerRegistry) { + reg.Register(&BlobHandler{"mine"}, internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(&BlobHandler{"repo"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) + reg.Register(&BlobHandler{"all"}, internal.ForRepo(internal.CONTEXT_TYPE, REPO), internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(&BlobHandler{"high"}, internal.WithPrio(internal.DEFAULT_BLOBHANDLER_PRIO+1)) + + h := (*eff).LookupHandler(IMPL, ART, mime.MIME_TEXT) + Expect(h).NotTo(BeNil()) + _, err := h.StoreBlob(nil, "", "", nil, nil) + Expect(err).To(MatchError(fmt.Errorf("high"))) + }, + Entry("plain", ®), + Entry("extended", &ext), + ) + + DescribeTable("copies registries", + func(eff *internal.BlobHandlerRegistry) { + mine := &BlobHandler{"mine"} + repo := &BlobHandler{"repo"} + reg.Register(mine, internal.ForMimeType(mime.MIME_TEXT)) + reg.Register(repo, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) + + h := (*eff).LookupHandler(internal.ImplementationRepositoryType{internal.CONTEXT_TYPE, REPO}, ART, mime.MIME_OCTET) + Expect(h).To(Equal(internal.MultiBlobHandler{repo})) + + copy := (*eff).Copy() + new := &BlobHandler{"repo2"} + copy.Register(new, internal.ForRepo(internal.CONTEXT_TYPE, REPO)) + + h = (*eff).LookupHandler(internal.ImplementationRepositoryType{internal.CONTEXT_TYPE, REPO}, ART, mime.MIME_OCTET) + Expect(h).To(Equal(internal.MultiBlobHandler{repo})) + + h = copy.LookupHandler(internal.ImplementationRepositoryType{internal.CONTEXT_TYPE, REPO}, ART, mime.MIME_OCTET) + Expect(h).To(Equal(internal.MultiBlobHandler{new})) + + }, + Entry("plain", ®), + Entry("extended", &ext), + ) + }) }) diff --git a/pkg/contexts/ocm/internal/builder.go b/pkg/contexts/ocm/internal/builder.go index 43475ca2b..0b7489aec 100644 --- a/pkg/contexts/ocm/internal/builder.go +++ b/pkg/contexts/ocm/internal/builder.go @@ -7,10 +7,12 @@ package internal import ( "context" "fmt" + "sync" "github.com/open-component-model/ocm/pkg/contexts/credentials" "github.com/open-component-model/ocm/pkg/contexts/datacontext" "github.com/open-component-model/ocm/pkg/contexts/oci" + "github.com/open-component-model/ocm/pkg/runtime" ) type Builder struct { @@ -167,15 +169,32 @@ func (b Builder) New(m ...datacontext.BuilderMode) Context { } } - if ociimpl != nil { - def, err := ociimpl(b.oci) + b.reposcheme = NewRepositoryTypeScheme(&delegatingDecoder{oci: b.oci}, b.reposcheme) + return newContext(b.credentials, b.oci, b.reposcheme, b.accessscheme, b.spechandlers, + b.blobhandlers, b.blobdigesters, b.credentials.ConfigContext().LoggingContext()) +} + +type delegatingDecoder struct { + lock sync.Mutex + oci oci.Context + delegate runtime.TypedObjectDecoder +} + +var _ runtime.TypedObjectDecoder = (*delegatingDecoder)(nil) + +func (d *delegatingDecoder) Decode(data []byte, unmarshaler runtime.Unmarshaler) (runtime.TypedObject, error) { + d.lock.Lock() + defer d.lock.Unlock() + + if d.delegate == nil && ociimpl != nil { + def, err := ociimpl(d.oci) if err != nil { panic(fmt.Sprintf("cannot create oci default decoder: %s", err)) } - reposcheme := NewRepositoryTypeScheme(def) - reposcheme.AddKnownTypes(b.reposcheme) // TODO: implement delegation - b.reposcheme = reposcheme + d.delegate = def } - return newContext(b.credentials, b.oci, b.reposcheme, b.accessscheme, b.spechandlers, - b.blobhandlers, b.blobdigesters, b.credentials.ConfigContext().LoggingContext()) + if d.delegate != nil { + return d.delegate.Decode(data, unmarshaler) + } + return nil, nil } diff --git a/pkg/contexts/ocm/internal/builder_test.go b/pkg/contexts/ocm/internal/builder_test.go index a1d818f23..2bd8d2e8e 100644 --- a/pkg/contexts/ocm/internal/builder_test.go +++ b/pkg/contexts/ocm/internal/builder_test.go @@ -12,7 +12,9 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/credentials" "github.com/open-component-model/ocm/pkg/contexts/datacontext" "github.com/open-component-model/ocm/pkg/contexts/oci" + "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" local "github.com/open-component-model/ocm/pkg/contexts/ocm/internal" + "github.com/open-component-model/ocm/pkg/runtime" ) var _ = Describe("builder test", func() { @@ -21,7 +23,7 @@ var _ = Describe("builder test", func() { Expect(ctx.AttributesContext()).To(BeIdenticalTo(datacontext.DefaultContext)) Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext)) - Expect(ctx.RepositoryTypes()).To(BeIdenticalTo(local.DefaultRepositoryTypeScheme)) + Expect(BaseRepoTypes(ctx.RepositoryTypes())).To(BeIdenticalTo(local.DefaultRepositoryTypeScheme)) Expect(ctx.AccessMethods()).To(BeIdenticalTo(local.DefaultAccessTypeScheme)) Expect(ctx.RepositorySpecHandlers()).To(BeIdenticalTo(local.DefaultRepositorySpecHandlers)) Expect(ctx.BlobHandlers()).To(BeIdenticalTo(local.DefaultBlobHandlerRegistry)) @@ -39,7 +41,7 @@ var _ = Describe("builder test", func() { Expect(ctx.AttributesContext()).NotTo(BeIdenticalTo(datacontext.DefaultContext)) Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext)) - Expect(ctx.RepositoryTypes()).To(BeIdenticalTo(local.DefaultRepositoryTypeScheme)) + Expect(BaseRepoTypes(ctx.RepositoryTypes())).To(BeIdenticalTo(local.DefaultRepositoryTypeScheme)) Expect(ctx.AccessMethods()).To(BeIdenticalTo(local.DefaultAccessTypeScheme)) Expect(ctx.RepositorySpecHandlers()).To(BeIdenticalTo(local.DefaultRepositorySpecHandlers)) Expect(ctx.BlobHandlers()).To(BeIdenticalTo(local.DefaultBlobHandlerRegistry)) @@ -98,5 +100,11 @@ var _ = Describe("builder test", func() { Expect(ctx.CredentialsContext()).NotTo(BeIdenticalTo(credentials.DefaultContext())) Expect(len(ctx.CredentialsContext().RepositoryTypes().KnownTypeNames())).To(Equal(0)) }) - }) + +func BaseRepoTypes(r cpi.RepositoryTypeScheme) runtime.Scheme { + if m, ok := r.(runtime.BaseScheme); ok { + return m.BaseScheme() + } + return nil +} diff --git a/pkg/contexts/ocm/internal/context.go b/pkg/contexts/ocm/internal/context.go index a7d96071e..b42be1cf0 100644 --- a/pkg/contexts/ocm/internal/context.go +++ b/pkg/contexts/ocm/internal/context.go @@ -19,6 +19,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ctf" "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" "github.com/open-component-model/ocm/pkg/runtime" + "github.com/open-component-model/ocm/pkg/utils" ) const CONTEXT_TYPE = "ocm" + datacontext.OCM_CONTEXT_SUFFIX @@ -53,6 +54,16 @@ type Context interface { GetAlias(name string) RepositorySpec SetAlias(name string, spec RepositorySpec) + + // Finalize finalizes elements implicitly opened during resource operations. + // For example, registered blob handler may open objects, which are kept open + // for performance reasons. At the end of a usage finalize should be called + // to finalize those elements. This method can be called any time by a context + // user to cleanup temporarily allocated resources. Therefore, only + // elements should be added to the finalzer, which can be reopened/created + // on-the fly whenever required. + Finalize() error + Finalizer() *utils.Finalizer } //////////////////////////////////////////////////////////////////////////////// @@ -94,6 +105,7 @@ type _context struct { blobHandlers BlobHandlerRegistry blobDigesters BlobDigesterRegistry aliases map[string]RepositorySpec + finalizer utils.Finalizer } var _ Context = &_context{} @@ -123,6 +135,14 @@ func (c *_context) Update() error { return c.updater.Update() } +func (c *_context) Finalize() error { + return c.finalizer.Finalize() +} + +func (c *_context) Finalizer() *utils.Finalizer { + return &c.finalizer +} + func (c *_context) AttributesContext() datacontext.AttributesContext { return c.sharedattributes } diff --git a/pkg/contexts/ocm/internal/repotypes.go b/pkg/contexts/ocm/internal/repotypes.go index 0f6d38ebc..bf3bf1b73 100644 --- a/pkg/contexts/ocm/internal/repotypes.go +++ b/pkg/contexts/ocm/internal/repotypes.go @@ -58,6 +58,10 @@ func NewRepositoryTypeScheme(defaultRepoDecoder runtime.TypedObjectDecoder, base return &repositoryTypeScheme{scheme} } +func (t *repositoryTypeScheme) BaseScheme() runtime.Scheme { + return t.SchemeBase.(runtime.BaseScheme).BaseScheme() +} + func (t *repositoryTypeScheme) AddKnownTypes(s RepositoryTypeScheme) { t.SchemeBase.AddKnownTypes(s) } diff --git a/pkg/contexts/ocm/plugin/cache/exec.go b/pkg/contexts/ocm/plugin/cache/exec.go index bb25909f7..8b1e1c589 100644 --- a/pkg/contexts/ocm/plugin/cache/exec.go +++ b/pkg/contexts/ocm/plugin/cache/exec.go @@ -34,10 +34,15 @@ func Exec(execpath string, config json.RawMessage, r io.Reader, w io.Writer, arg if err != nil { var result cmds.Error var msg string - if err := json.Unmarshal(stderr.Bytes(), &result); err == nil { - msg = result.Error + data := stderr.Bytes() + if len(data) == 0 { + msg = err.Error() } else { - msg = fmt.Sprintf("[%s]", string(stderr.Bytes())) + if err := json.Unmarshal(stderr.Bytes(), &result); err == nil { + msg = result.Error + } else { + msg = fmt.Sprintf("[%s]", string(stderr.Bytes())) + } } return nil, fmt.Errorf("%s", msg) } diff --git a/pkg/contexts/ocm/plugin/cache/plugin.go b/pkg/contexts/ocm/plugin/cache/plugin.go index bfa6c6888..0220fb345 100644 --- a/pkg/contexts/ocm/plugin/cache/plugin.go +++ b/pkg/contexts/ocm/plugin/cache/plugin.go @@ -13,6 +13,7 @@ type Plugin = *pluginImpl // //nolint: errname // is no error. type pluginImpl struct { name string + source *PluginSource descriptor *internal.Descriptor path string error string @@ -21,15 +22,24 @@ type pluginImpl struct { } func NewPlugin(name string, path string, desc *internal.Descriptor, errmsg string) Plugin { - return &pluginImpl{ + p := &pluginImpl{ name: name, path: path, descriptor: desc, error: errmsg, - - uploaders: NewConstraintRegistry[internal.UploaderDescriptor, internal.UploaderKey](desc.Uploaders), - downloaders: NewConstraintRegistry[internal.DownloaderDescriptor, internal.DownloaderKey](desc.Downloaders), } + if desc != nil { + p.uploaders = NewConstraintRegistry[internal.UploaderDescriptor, internal.UploaderKey](desc.Uploaders) + p.downloaders = NewConstraintRegistry[internal.DownloaderDescriptor, internal.DownloaderKey](desc.Downloaders) + } else { + p.uploaders = NewConstraintRegistry[internal.UploaderDescriptor, internal.UploaderKey](nil) + p.downloaders = NewConstraintRegistry[internal.DownloaderDescriptor, internal.DownloaderKey](nil) + } + return p +} + +func (p *pluginImpl) GetSource() *PluginSource { + return p.source } func (p *pluginImpl) GetDescriptor() *internal.Descriptor { @@ -83,7 +93,7 @@ func (p *pluginImpl) GetAccessMethodDescriptor(name, version string) *internal.A return nil } -func (p *pluginImpl) LookupDownloader(name string, artType, mediaType string) []*internal.DownloaderDescriptor { +func (p *pluginImpl) LookupDownloader(name string, artType, mediaType string) internal.List[*internal.DownloaderDescriptor] { if !p.IsValid() { return nil } @@ -98,7 +108,7 @@ func (p *pluginImpl) GetDownloaderDescriptor(name string) *internal.DownloaderDe return p.descriptor.Downloaders.Get(name) } -func (p *pluginImpl) LookupUploader(name string, artType, mediaType string) []*internal.UploaderDescriptor { +func (p *pluginImpl) LookupUploader(name string, artType, mediaType string) internal.List[*internal.UploaderDescriptor] { if !p.IsValid() { return nil } @@ -106,6 +116,26 @@ func (p *pluginImpl) LookupUploader(name string, artType, mediaType string) []*i return p.uploaders.LookupFor(name, internal.UploaderKey{}.SetArtefact(artType, mediaType)) } +func (p *pluginImpl) LookupUploadersForKeys(name string, keys internal.UploaderKeySet) internal.List[*internal.UploaderDescriptor] { + if !p.IsValid() { + return nil + } + + var r internal.List[*internal.UploaderDescriptor] + for k := range keys { + r = r.MergeWith(p.uploaders.LookupFor(name, k)) + } + return r +} + +func (p *pluginImpl) LookupUploaderKeys(name string, artType, mediaType string) internal.UploaderKeySet { + if !p.IsValid() { + return nil + } + + return p.uploaders.LookupKeysFor(name, internal.UploaderKey{}.SetArtefact(artType, mediaType)) +} + func (p *pluginImpl) GetUploaderDescriptor(name string) *internal.UploaderDescriptor { if !p.IsValid() { return nil diff --git a/pkg/contexts/ocm/plugin/cache/plugindir.go b/pkg/contexts/ocm/plugin/cache/plugindir.go index 22e333794..342ab67dc 100644 --- a/pkg/contexts/ocm/plugin/cache/plugindir.go +++ b/pkg/contexts/ocm/plugin/cache/plugindir.go @@ -57,6 +57,10 @@ func (c *pluginDirImpl) Get(name string) Plugin { func (c *pluginDirImpl) add(name string, desc *internal.Descriptor, path string, errmsg string, list *errors.ErrorList) { c.plugins[name] = NewPlugin(name, path, desc, errmsg) + if path != "" { + src, _ := ReadPluginSource(filepath.Dir(path), filepath.Base(path)) + c.plugins[name].source = src + } if errmsg != "" && list != nil { list.Add(fmt.Errorf("%s: %s", name, errmsg)) } @@ -73,25 +77,32 @@ func (c *pluginDirImpl) scan(path string) error { for _, fi := range entries { if fi.Mode()&0o001 != 0 { execpath := filepath.Join(path, fi.Name()) - result, err := Exec(execpath, nil, nil, nil, info.NAME) + desc, err := GetPluginInfo(execpath) if err != nil { c.add(fi.Name(), nil, execpath, err.Error(), list) continue } - // TODO: Version handling by scheme - var desc internal.Descriptor - if err = json.Unmarshal(result, &desc); err != nil { - c.add(fi.Name(), nil, execpath, fmt.Sprintf("cannot unmarshal plugin descriptor: %s", err.Error()), list) - continue - } - if desc.PluginName != fi.Name() { c.add(fi.Name(), nil, execpath, fmt.Sprintf("nmatching plugin name %q", desc.PluginName), list) continue } - c.add(desc.PluginName, &desc, execpath, "", nil) + c.add(desc.PluginName, desc, execpath, "", nil) } } return list.Result() } + +func GetPluginInfo(execpath string) (*internal.Descriptor, error) { + result, err := Exec(execpath, nil, nil, nil, info.NAME) + if err != nil { + return nil, err + } + + // TODO: Version handling by scheme + var desc internal.Descriptor + if err = json.Unmarshal(result, &desc); err != nil { + return nil, errors.Wrapf(err, "cannot unmarshal plugin descriptor: %s", err.Error()) + } + return &desc, nil +} diff --git a/pkg/contexts/ocm/plugin/cache/registry.go b/pkg/contexts/ocm/plugin/cache/registry.go index ddcfc2f1e..c24869872 100644 --- a/pkg/contexts/ocm/plugin/cache/registry.go +++ b/pkg/contexts/ocm/plugin/cache/registry.go @@ -7,6 +7,7 @@ package cache import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/internal" "github.com/open-component-model/ocm/pkg/contexts/ocm/utils/registry" + "github.com/open-component-model/ocm/pkg/generics" ) type ConstraintRegistry[T any, K registry.Key[K]] struct { @@ -15,7 +16,11 @@ type ConstraintRegistry[T any, K registry.Key[K]] struct { } func (r *ConstraintRegistry[T, K]) Lookup(key K) []*T { - return r.mapping.GetHandler(key) + return r.mapping.LookupHandler(key) +} + +func (r *ConstraintRegistry[T, K]) LookupKeys(key K) generics.Set[K] { + return r.mapping.LookupKeys(key) } func (r *ConstraintRegistry[T, K]) LookupFor(name string, key K) []*T { @@ -26,7 +31,18 @@ func (r *ConstraintRegistry[T, K]) LookupFor(name string, key K) []*T { if m == nil { return nil } - return m.GetHandler(key) + return m.LookupHandler(key) +} + +func (r *ConstraintRegistry[T, K]) LookupKeysFor(name string, key K) generics.Set[K] { + if name == "" { + return r.LookupKeys(key) + } + m := r.elems[name] + if m == nil { + return nil + } + return m.LookupKeys(key) } func NewConstraintRegistry[T internal.Element[K], K registry.Key[K]](list []T) *ConstraintRegistry[T, K] { @@ -36,10 +52,15 @@ func NewConstraintRegistry[T internal.Element[K], K registry.Key[K]](list []T) * for i := range list { d := list[i] nested := registry.NewRegistry[*T, K]() - for _, c := range d.GetConstraints() { - if c.IsValid() { - reg.Register(c, &d) - nested.Register(c, &d) + if len(d.GetConstraints()) == 0 { + var zero K + nested.Register(zero, &d) + } else { + for _, c := range d.GetConstraints() { + if c.IsValid() { + reg.Register(c, &d) + nested.Register(c, &d) + } } } m[d.GetName()] = nested diff --git a/pkg/contexts/ocm/plugin/cache/updater.go b/pkg/contexts/ocm/plugin/cache/updater.go new file mode 100644 index 000000000..5c99b13b3 --- /dev/null +++ b/pkg/contexts/ocm/plugin/cache/updater.go @@ -0,0 +1,331 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package cache + +import ( + "encoding/json" + "fmt" + "io" + "os" + "runtime" + + "github.com/Masterminds/semver/v3" + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "sigs.k8s.io/yaml" + + "github.com/open-component-model/ocm/pkg/common" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugindirattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/download" + "github.com/open-component-model/ocm/pkg/errors" + "github.com/open-component-model/ocm/pkg/semverutils" + utils2 "github.com/open-component-model/ocm/pkg/utils" +) + +type PluginSource struct { + Repository *cpi.GenericRepositorySpec `json:"repository"` + Component string `json:"component"` + Version string `json:"version"` + Resource string `json:"resource"` +} + +type PluginUpdater struct { + Context ocm.Context + Force bool + RemoveMode bool + UpdateMode bool + Describe bool + Constraints []*semver.Constraints + + Current string + Printer common.Printer +} + +func NewPluginUpdater(ctx ocm.ContextProvider, printer common.Printer) *PluginUpdater { + if printer == nil { + printer = common.NewPrinter(nil) + } + return &PluginUpdater{ + Context: ctx.OCMContext(), + Printer: printer, + } +} + +func (o *PluginUpdater) SetupCurrent(name string) error { + dir := plugindirattr.Get(o.Context) + if dir == "" { + return fmt.Errorf("no plugin dir configured") + } + src, err := ReadPluginSource(dir, name) + if err != nil { + return nil + } + o.Current = src.Version + return nil +} + +func (o *PluginUpdater) Remove(session ocm.Session, name string) error { + dir := plugindirattr.Get(o.Context) + if dir == "" { + return fmt.Errorf("no plugin dir configured") + } + if err := RemovePluginSource(dir, name); err != nil { + return errors.Wrapf(err, "cannot remove source info for plugin %q", name) + } + file := filepath.Join(dir, name) + if ok, err := vfs.FileExists(osfs.New(), file); !ok && err == nil { + return fmt.Errorf("plugin %s not found", name) + } + if err := RemoveFile(file); err != nil { + return errors.Wrapf(err, "cannot remove plugin %q", name) + } + o.Printer.Printf("plugin %s removed\n", name) + return nil +} + +func (o *PluginUpdater) Update(session ocm.Session, name string) error { + dir := plugindirattr.Get(o.Context) + if dir == "" { + return fmt.Errorf("no plugin dir configured") + } + src, err := ReadPluginSource(dir, name) + if err != nil { + return err + } + o.Current = src.Version + repo, err := session.LookupRepository(o.Context, src.Repository) + if err != nil { + return err + } + comp, err := session.LookupComponent(repo, src.Component) + if err != nil { + return err + } + return o.downloadLatest(session, comp, src.Resource) +} + +func (o *PluginUpdater) DownloadRef(session ocm.Session, ref string, name string) error { + result, err := session.EvaluateVersionRef(o.Context, ref) + if err != nil { + return err + } + if result.Component == nil { + return fmt.Errorf("component required") + } + if result.Version == nil { + return o.downloadLatest(session, result.Component, name) + } + if result.Version.GetVersion() == o.Current && !o.Force { + o.Printer.Printf("plugin %s already uptodate\n", name) + return nil + } + return o.download(session, result.Version, name) +} + +func (o *PluginUpdater) DownloadFromRepo(session ocm.Session, repo ocm.Repository, ref, name string) error { + cr, err := ocm.ParseComp(ref) + if err != nil { + return err + } + + var cv ocm.ComponentVersionAccess + comp, err := session.LookupComponent(repo, cr.Component) + if err != nil { + return err + } + if cr.IsVersion() { + if *cr.Version == o.Current && !o.Force { + o.Printer.Printf("plugin %s already uptodate\n", name) + return nil + } + cv, err = session.GetComponentVersion(comp, *cr.Version) + if err != nil { + return err + } + return o.download(session, cv, name) + } + return o.downloadLatest(session, comp, name) +} + +func (o *PluginUpdater) downloadLatest(session ocm.Session, comp ocm.ComponentAccess, name string) error { + var vers []string + + vers, err := comp.ListVersions() + if err != nil { + return errors.Wrapf(err, "cannot list versions for component %s", comp.GetName()) + } + if len(vers) == 0 { + return errors.Wrapf(err, "no versions found for component %s", comp.GetName()) + } + + versions, _ := semverutils.MatchVersionStrings(vers, o.Constraints...) + if len(versions) == 0 { + return fmt.Errorf("no versions for component %s match the constraints", comp.GetName()) + } + if len(versions) > 1 { + versions = versions[len(versions)-1:] + } + if versions[0].Original() == o.Current && !o.Force { + o.Printer.Printf("plugin %s already uptodate\n", name) + return nil + } + cv, err := session.GetComponentVersion(comp, versions[0].Original()) + if err != nil { + return err + } + return o.download(session, cv, name) +} + +func (o *PluginUpdater) download(session ocm.Session, cv ocm.ComponentVersionAccess, name string) (err error) { + defer errors.PropagateErrorf(&err, nil, "%s", common.VersionedElementKey(cv)) + + var found ocm.ResourceAccess + var wrong ocm.ResourceAccess + for _, r := range cv.GetResources() { + if name != "" && r.Meta().Name != name { + continue + } + if r.Meta().Type == "ocmPlugin" { + if r.Meta().ExtraIdentity.Get("os") == runtime.GOOS && + r.Meta().ExtraIdentity.Get("architecture") == runtime.GOARCH { + found = r + break + } + wrong = r + } else { //nolint: gocritic // yes + if name != "" { + wrong = r + } + } + } + if found == nil { + if wrong != nil { + if wrong.Meta().Type != "ocmPlugin" { + return fmt.Errorf("resource %q has wrong type: %s", wrong.Meta().Name, wrong.Meta().Type) + } + return fmt.Errorf("os %s architecture %s not found for resource %q", runtime.GOOS, runtime.GOARCH, wrong.Meta().Name) + } + if name != "" { + return fmt.Errorf("resource %q not found", name) + } + return fmt.Errorf("no ocmPlugin found") + } + o.Printer.Printf("found resource %s[%s]\n", found.Meta().Name, found.Meta().ExtraIdentity.String()) + + file, err := os.CreateTemp(os.TempDir(), "plugin-*") + if err != nil { + return errors.Wrapf(err, "cannot create temp file") + } + file.Close() + fs := osfs.New() + _, _, err = download.For(o.Context).Download(o.Printer, found, file.Name(), fs) + if err != nil { + return errors.Wrapf(err, "cannot download resource %s", found.Meta().Name) + } + + desc, err := GetPluginInfo(file.Name()) + if err != nil { + return err + } + if o.Describe { + data, err := yaml.Marshal(desc) + if err != nil { + return errors.Wrapf(err, "cannot marshal plugin descriptor") + } + o.Printer.Printf("%s", string(data)) + } else { + err := o.SetupCurrent(desc.PluginName) + if err != nil { + return err + } + if cv.GetVersion() == o.Current { + o.Printer.Printf("version %s already installed\n", o.Current) + if !o.Force { + return nil + } + } + dir := plugindirattr.Get(o.Context) + if dir != "" { + target := filepath.Join(dir, desc.PluginName) + + verb := "installing" + if ok, _ := vfs.FileExists(fs, target); ok { + if !o.Force && (cv.GetVersion() == o.Current || !o.UpdateMode) { + return fmt.Errorf("plugin %s already found in %s", desc.PluginName, dir) + } + if o.UpdateMode { + verb = "updating" + } + fs.Remove(target) + } + o.Printer.Printf("%s plugin %s[%s] in %s...\n", verb, desc.PluginName, desc.PluginVersion, dir) + dst, err := fs.OpenFile(target, vfs.O_CREATE|vfs.O_TRUNC|vfs.O_WRONLY, 0o755) + if err != nil { + return errors.Wrapf(err, "cannot create plugin file %s", target) + } + defer dst.Close() + src, err := fs.OpenFile(file.Name(), vfs.O_RDONLY, 0) + if err != nil { + return errors.Wrapf(err, "cannot open plugin executable %s", file.Name()) + } + _, err = io.Copy(dst, src) + utils2.IgnoreError(src.Close()) + utils2.IgnoreError(os.Remove(file.Name())) + utils2.IgnoreError(WritePluginSource(dir, cv, found.Meta().Name, desc.PluginName)) + if err != nil { + return errors.Wrapf(err, "cannot copy plugin file %s", target) + } + } + } + return nil +} + +func RemoveFile(file string) error { + if ok, err := vfs.FileExists(osfs.New(), file); !ok || err != nil { + return err + } + return os.Remove(file) +} + +func RemovePluginSource(dir string, name string) error { + return RemoveFile(filepath.Join(dir, "."+name+".info")) +} + +func WritePluginSource(dir string, cv ocm.ComponentVersionAccess, rsc, name string) error { + spec, err := cpi.ToGenericRepositorySpec(cv.Repository().GetSpecification()) + if err != nil { + return err + } + cv.Repository().GetSpecification() + src := &PluginSource{ + Repository: spec, + Component: cv.GetName(), + Version: cv.GetVersion(), + Resource: rsc, + } + + data, err := json.Marshal(src) + if err != nil { + return err + } + //nolint: gosec // yes + return os.WriteFile(filepath.Join(dir, "."+name+".info"), data, 0o644) +} + +func ReadPluginSource(dir string, name string) (*PluginSource, error) { + data, err := os.ReadFile(filepath.Join(dir, "."+name+".info")) + if err != nil { + return nil, fmt.Errorf("no source information available for plugin %s", name) + } + + var src PluginSource + if err := json.Unmarshal(data, &src); err != nil { + return nil, errors.Wrapf(err, "cannot unmarshal source information") + } + return &src, nil +} diff --git a/pkg/contexts/ocm/plugin/config/type.go b/pkg/contexts/ocm/plugin/config/type.go index 3e028a3ac..ed3b89d0c 100644 --- a/pkg/contexts/ocm/plugin/config/type.go +++ b/pkg/contexts/ocm/plugin/config/type.go @@ -18,8 +18,8 @@ const ( ) func init() { - cfgcpi.RegisterConfigType(ConfigType, cfgcpi.NewConfigType(ConfigType, &Config{})) - cfgcpi.RegisterConfigType(ConfigTypeV1, cfgcpi.NewConfigType(ConfigTypeV1, &Config{})) + cfgcpi.RegisterConfigType(ConfigType, cfgcpi.NewConfigType(ConfigType, &Config{}, usage)) + cfgcpi.RegisterConfigType(ConfigTypeV1, cfgcpi.NewConfigType(ConfigTypeV1, &Config{}, usage)) } // Config describes a memory based config interface. @@ -54,3 +54,14 @@ func (a *Config) ApplyTo(ctx config.Context, target interface{}) error { type Target interface { ConfigurePlugin(name string, config json.RawMessage) } + +const usage = ` +The config type ` + ConfigType + ` can be used to configure a +plugin. + +
+    type: ` + ConfigType + `
+    plugin: <plugin name>
+    config: <arbitrary configuration structure>
+
+` diff --git a/pkg/contexts/ocm/plugin/interface.go b/pkg/contexts/ocm/plugin/interface.go index 73400834f..987b61590 100644 --- a/pkg/contexts/ocm/plugin/interface.go +++ b/pkg/contexts/ocm/plugin/interface.go @@ -24,5 +24,6 @@ type ( DownloaderKey = internal.DownloaderKey UploaderDescriptor = internal.UploaderDescriptor UploaderKey = internal.UploaderKey + UploaderKeySet = internal.UploaderKeySet UploadTargetSpecInfo = internal.UploadTargetSpecInfo ) diff --git a/pkg/contexts/ocm/plugin/internal/descriptor.go b/pkg/contexts/ocm/plugin/internal/descriptor.go index 157b4f767..a1d96c4a2 100644 --- a/pkg/contexts/ocm/plugin/internal/descriptor.go +++ b/pkg/contexts/ocm/plugin/internal/descriptor.go @@ -30,13 +30,17 @@ func NewDownloaderKey(arttype, mediatype string) DownloaderKey { type DownloaderDescriptor struct { Name string `json:"name"` Description string `json:"description"` - Constraints []DownloaderKey `json:"constraints"` + Constraints []DownloaderKey `json:"constraints,omitempty"` } func (d DownloaderDescriptor) GetName() string { return d.Name } +func (d DownloaderDescriptor) GetDescription() string { + return d.Description +} + func (d DownloaderDescriptor) GetConstraints() []DownloaderKey { return d.Constraints } @@ -44,13 +48,17 @@ func (d DownloaderDescriptor) GetConstraints() []DownloaderKey { type UploaderDescriptor struct { Name string `json:"name"` Description string `json:"description"` - Constraints []UploaderKey `json:"constraints"` + Constraints []UploaderKey `json:"constraints,omitempty"` } func (d UploaderDescriptor) GetName() string { return d.Name } +func (d UploaderDescriptor) GetDescription() string { + return d.Description +} + func (d UploaderDescriptor) GetConstraints() []UploaderKey { return d.Constraints } @@ -60,7 +68,7 @@ type AccessMethodDescriptor struct { Version string `json:"version,omitempty"` Description string `json:"description"` Format string `json:"format"` - CLIOptions []CLIOption `json:"options"` + CLIOptions []CLIOption `json:"options,omitempty"` } type CLIOption struct { diff --git a/pkg/contexts/ocm/plugin/internal/keys.go b/pkg/contexts/ocm/plugin/internal/keys.go index c7eb5b4f7..ed07ec81e 100644 --- a/pkg/contexts/ocm/plugin/internal/keys.go +++ b/pkg/contexts/ocm/plugin/internal/keys.go @@ -6,6 +6,8 @@ package internal import ( "fmt" + + "github.com/open-component-model/ocm/pkg/generics" ) type RepositoryContext struct { @@ -28,6 +30,13 @@ func (k RepositoryContext) String() string { return "" } +func (k RepositoryContext) Describe() string { + if k.HasRepo() { + return fmt.Sprintf("Default Repository Upload:\n Context Type: %s\n RepositoryType: %s", k.ContextType, k.RepositoryType) + } + return "" +} + type ArtefactContext struct { ArtifactType string `json:"artifactType"` MediaType string `json:"mediaType"` @@ -49,6 +58,10 @@ func (k ArtefactContext) String() string { return fmt.Sprintf("%s:%s", k.ArtifactType, k.MediaType) } +func (k ArtefactContext) Describe() string { + return fmt.Sprintf("Artefact Type: %s\nMedia Type :%s", k.ArtifactType, k.MediaType) +} + func (k ArtefactContext) SetArtefact(arttype, mediatype string) ArtefactContext { k.ArtifactType = arttype k.MediaType = mediatype @@ -68,6 +81,10 @@ func (k UploaderKey) String() string { return fmt.Sprintf("%s%s", k.ArtefactContext.String(), k.RepositoryContext.String()) } +func (k UploaderKey) Describe() string { + return fmt.Sprintf("%s%s", k.ArtefactContext.Describe(), k.RepositoryContext.Describe()) +} + func (k UploaderKey) SetArtefact(arttype, mediatype string) UploaderKey { k.ArtifactType = arttype k.MediaType = mediatype @@ -79,3 +96,5 @@ func (k UploaderKey) SetRepo(contexttype, repotype string) UploaderKey { k.RepositoryType = repotype return k } + +type UploaderKeySet = generics.Set[UploaderKey] diff --git a/pkg/contexts/ocm/plugin/internal/suite_test.go b/pkg/contexts/ocm/plugin/internal/suite_test.go new file mode 100644 index 000000000..ba2389e5a --- /dev/null +++ b/pkg/contexts/ocm/plugin/internal/suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package internal_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugin Internal Test Suite") +} diff --git a/pkg/contexts/ocm/plugin/internal/utils.go b/pkg/contexts/ocm/plugin/internal/utils.go index 00f6145b2..84279facf 100644 --- a/pkg/contexts/ocm/plugin/internal/utils.go +++ b/pkg/contexts/ocm/plugin/internal/utils.go @@ -5,6 +5,8 @@ package internal import ( + "sort" + "github.com/open-component-model/ocm/pkg/contexts/ocm/utils/registry" ) @@ -12,6 +14,12 @@ type Named interface { GetName() string } +type StringName string + +func (e StringName) GetName() string { + return string(e) +} + type Element[K registry.Key[K]] interface { Named GetConstraints() []K @@ -27,3 +35,26 @@ func (l List[T]) Get(name string) *T { } return nil } + +func (l List[T]) GetNames() []string { + var n []string + for _, e := range l { + n = append(n, e.GetName()) + } + sort.Strings(n) + return n +} + +func (l List[T]) MergeWith(o List[T]) List[T] { + var list []T +next: + for _, e := range o { + for _, f := range l { + if e.GetName() == f.GetName() { + continue next + } + } + list = append(list, e) + } + return append(append(l[:0:0], l...), list...) +} diff --git a/pkg/contexts/ocm/plugin/internal/utils_test.go b/pkg/contexts/ocm/plugin/internal/utils_test.go new file mode 100644 index 000000000..2d0b9420d --- /dev/null +++ b/pkg/contexts/ocm/plugin/internal/utils_test.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("utils", func() { + + It("merges lists", func() { + l1 := List[StringName]{"a", "b"} + l2 := List[StringName]{"b", "c"} + l3 := List[StringName]{"a", "d"} + + m2 := l1.MergeWith(l2) + Expect(m2).To(Equal(List[StringName]{"a", "b", "c"})) + Expect(l1.MergeWith(l3)).To(Equal(List[StringName]{"a", "b", "d"})) + Expect(m2).To(Equal(List[StringName]{"a", "b", "c"})) + }) +}) diff --git a/pkg/contexts/ocm/plugin/plugin.go b/pkg/contexts/ocm/plugin/plugin.go index af675caf1..c3e08aab8 100644 --- a/pkg/contexts/ocm/plugin/plugin.go +++ b/pkg/contexts/ocm/plugin/plugin.go @@ -114,13 +114,13 @@ func (p *pluginImpl) ComposeAccessMethod(name string, opts flagsets.ConfigOption func (p *pluginImpl) ValidateUploadTarget(name string, spec []byte) (*ppi.UploadTargetSpecInfo, error) { result, err := p.Exec(nil, nil, upload.Name, uplval.Name, name, string(spec)) if err != nil { - return nil, errors.Wrapf(err, "plugin %s", p.Name()) + return nil, errors.Wrapf(err, "plugin uploader %s/%s", p.Name(), name) } var info ppi.UploadTargetSpecInfo err = json.Unmarshal(result, &info) if err != nil { - return nil, errors.Wrapf(err, "plugin %s: cannot unmarshal upload target info", p.Name()) + return nil, errors.Wrapf(err, "plugin uploader %s/%s: cannot unmarshal upload target info", p.Name(), name) } return &info, nil } diff --git a/pkg/contexts/ocm/plugin/plugin_test.go b/pkg/contexts/ocm/plugin/plugin_test.go index fd5ce148b..a0e3a0355 100644 --- a/pkg/contexts/ocm/plugin/plugin_test.go +++ b/pkg/contexts/ocm/plugin/plugin_test.go @@ -18,6 +18,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" + "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" ) var _ = Describe("setup plugin cache", func() { @@ -54,7 +55,7 @@ var _ = Describe("setup plugin cache", func() { p := registry.Get("test") Expect(p).NotTo(BeNil()) Expect(len(p.GetDescriptor().AccessMethods)).To(Equal(2)) - Expect(registry.RegisterExtensions()).To(Succeed()) + Expect(registration.RegisterExtensions(registry.GetContext())).To(Succeed()) t := ctx.AccessMethods().GetAccessType("test") Expect(t).NotTo(BeNil()) raw := ` diff --git a/pkg/contexts/ocm/plugin/plugins/plugins.go b/pkg/contexts/ocm/plugin/plugins/plugins.go index b00508e24..b83b4cf83 100644 --- a/pkg/contexts/ocm/plugin/plugins/plugins.go +++ b/pkg/contexts/ocm/plugin/plugins/plugins.go @@ -9,15 +9,11 @@ import ( "sync" cfgcpi "github.com/open-component-model/ocm/pkg/contexts/config/cpi" - "github.com/open-component-model/ocm/pkg/contexts/ocm" - access "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/plugin" - blob "github.com/open-component-model/ocm/pkg/contexts/ocm/blobhandler/generic/plugin" "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/config" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/internal" - "github.com/open-component-model/ocm/pkg/runtime" "github.com/open-component-model/ocm/pkg/utils" ) @@ -27,7 +23,7 @@ type pluginsImpl struct { lock sync.RWMutex updater cfgcpi.Updater - ctx ocm.Context + ctx cpi.Context base cache.PluginDir configs map[string]json.RawMessage plugins map[string]plugin.Plugin @@ -35,7 +31,7 @@ type pluginsImpl struct { var _ config.Target = (*pluginsImpl)(nil) -func New(ctx ocm.Context, path string) Set { +func New(ctx cpi.Context, path string) Set { pi := &pluginsImpl{ ctx: ctx, configs: map[string]json.RawMessage{}, @@ -50,6 +46,10 @@ func New(ctx ocm.Context, path string) Set { return pi } +func (pi *pluginsImpl) GetContext() cpi.Context { + return pi.ctx +} + func (pi *pluginsImpl) Update() { err := pi.updater.Update() if err != nil { @@ -86,46 +86,3 @@ func (pi *pluginsImpl) Get(name string) plugin.Plugin { } return nil } - -// RegisterExtensions registers all the extension provided the found plugin -// at the given context. If no context is given, the cache context is used. -func (pi *pluginsImpl) RegisterExtensions() error { - pi.Update() - - pi.lock.RLock() - defer pi.lock.RUnlock() - - for _, p := range pi.plugins { - if !p.IsValid() { - continue - } - for _, m := range p.GetDescriptor().AccessMethods { - name := m.Name - if m.Version != "" { - name = name + runtime.VersionSeparator + m.Version - } - pi.ctx.Logger(internal.TAG).Debug("registering access method", - "plugin", p.Name(), - "type", name) - pi.ctx.AccessMethods().Register(name, access.NewType(name, p, &m)) - } - - for _, u := range p.GetDescriptor().Uploaders { - for _, c := range u.Constraints { - if c.ContextType != "" && c.RepositoryType != "" && c.MediaType != "" { - hdlr, err := blob.New(p, u.Name, nil) - if err != nil { - pi.ctx.Logger(internal.TAG).Error("cannot create blob handler fpr plugin", "plugin", p.Name(), "handler", u.Name) - } else { - pi.ctx.Logger(internal.TAG).Debug("registering repository blob handler", - "context", c.ContextType+":"+c.RepositoryType, - "plugin", p.Name(), - "handler", u.Name) - pi.ctx.BlobHandlers().Register(hdlr, cpi.ForRepo(c.ContextType, c.RepositoryType), cpi.ForMimeType(c.MediaType)) - } - } - } - } - } - return nil -} diff --git a/pkg/contexts/ocm/plugin/plugins/register.go b/pkg/contexts/ocm/plugin/plugins/register.go deleted file mode 100644 index cf748c8f6..000000000 --- a/pkg/contexts/ocm/plugin/plugins/register.go +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. -// -// SPDX-License-Identifier: Apache-2.0 - -package plugins - -import ( - "encoding/json" - "fmt" - - blobhdlr "github.com/open-component-model/ocm/pkg/contexts/ocm/blobhandler/generic/plugin" - "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" - "github.com/open-component-model/ocm/pkg/contexts/ocm/download" - downhdlr "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/plugin" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" - "github.com/open-component-model/ocm/pkg/errors" -) - -func (pi *pluginsImpl) RegisterBlobHandler(pname, name string, artType, mediaType string, target json.RawMessage) error { - p := pi.Get(pname) - if p == nil { - return errors.ErrUnknown(plugin.KIND_PLUGIN, pname) - } - d := p.LookupUploader(name, artType, mediaType) - if len(d) == 0 { - if name == "" { - return fmt.Errorf("no uploader found for [art:%q, media:%q]", artType, mediaType) - } - return fmt.Errorf("uploader %s not valid for [art:%q, media:%q]", name, artType, mediaType) - } - for _, e := range d { - h, err := blobhdlr.New(p, e.Name, target) - if err != nil { - return err - } - pi.ctx.BlobHandlers().Register(h, cpi.ForArtefactType(artType), cpi.ForMimeType(mediaType)) - } - return nil -} - -func (pi *pluginsImpl) RegisterDownloadHandler(pname, name string, artType, mediaType string) error { - p := pi.Get(pname) - if p == nil { - return errors.ErrUnknown(plugin.KIND_PLUGIN, pname) - } - d := p.LookupDownloader(name, artType, mediaType) - if len(d) == 0 { - if name == "" { - return fmt.Errorf("no downloader found for [art:%q, media:%q]", artType, mediaType) - } - return fmt.Errorf("downloader %s not valid for [art:%q, media:%q]", name, artType, mediaType) - } - for _, e := range d { - h, err := downhdlr.New(p, e.Name) - if err != nil { - return err - } - download.For(pi.ctx).Register(artType, mediaType, h) - } - return nil -} diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/get/cmd.go b/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/get/cmd.go index 96b51e421..5a5b79915 100644 --- a/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/get/cmd.go +++ b/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/get/cmd.go @@ -69,7 +69,7 @@ func (o *Options) Complete(args []string) error { func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { spec, err := p.DecodeAccessSpecification(opts.Specification) if err != nil { - return err + return errors.Wrapf(err, "access specification") } m := p.GetAccessMethod(spec.GetKind(), spec.GetVersion()) diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/validate/cmd.go b/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/validate/cmd.go index 9a4e2fa6e..e5fe3c5d7 100644 --- a/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/validate/cmd.go +++ b/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/validate/cmd.go @@ -87,7 +87,7 @@ type Result struct { func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { spec, err := p.DecodeAccessSpecification(opts.Specification) if err != nil { - return err + return errors.Wrapf(err, "access specification") } m := p.GetAccessMethod(spec.GetKind(), spec.GetVersion()) @@ -98,7 +98,7 @@ func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { if err != nil { return err } - result := Result{MediaType: info.MediaType, ConsumerId: info.ConsumerId, Hint: info.Hint} + result := Result{MediaType: info.MediaType, ConsumerId: info.ConsumerId, Hint: info.Hint, Short: info.Short} data, err := json.Marshal(result) if err != nil { return err diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/app.go b/pkg/contexts/ocm/plugin/ppi/cmds/app.go index b255ab01b..3bdfe1cf4 100644 --- a/pkg/contexts/ocm/plugin/ppi/cmds/app.go +++ b/pkg/contexts/ocm/plugin/ppi/cmds/app.go @@ -74,7 +74,7 @@ func NewPluginCommand(p ppi.Plugin) *PluginCommand { help.AddCommand(descriptor.New()) - p.Options().AddFlags(cmd.Flags()) + p.GetOptions().AddFlags(cmd.Flags()) pcmd.command = cmd return pcmd } diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/upload/put/cmd.go b/pkg/contexts/ocm/plugin/ppi/cmds/upload/put/cmd.go index 0530c44c4..a37e5e507 100644 --- a/pkg/contexts/ocm/plugin/ppi/cmds/upload/put/cmd.go +++ b/pkg/contexts/ocm/plugin/ppi/cmds/upload/put/cmd.go @@ -84,7 +84,7 @@ func (o *Options) Complete(args []string) error { func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { spec, err := p.DecodeUploadTargetSpecification(opts.Specification) if err != nil { - return err + return errors.Wrapf(err, "target specification") } u := p.GetUploader(opts.Name) diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/upload/validate/cmd.go b/pkg/contexts/ocm/plugin/ppi/cmds/upload/validate/cmd.go index 19142690b..0e691f17c 100644 --- a/pkg/contexts/ocm/plugin/ppi/cmds/upload/validate/cmd.go +++ b/pkg/contexts/ocm/plugin/ppi/cmds/upload/validate/cmd.go @@ -83,7 +83,7 @@ type Result struct { func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error { spec, err := p.DecodeUploadTargetSpecification(opts.Specification) if err != nil { - return err + return errors.Wrapf(err, "target specification") } m := p.GetUploader(opts.Name) diff --git a/pkg/contexts/ocm/plugin/ppi/interface.go b/pkg/contexts/ocm/plugin/ppi/interface.go index dafe9d3d9..dde385629 100644 --- a/pkg/contexts/ocm/plugin/ppi/interface.go +++ b/pkg/contexts/ocm/plugin/ppi/interface.go @@ -5,6 +5,7 @@ package ppi import ( + "encoding/json" "io" "github.com/open-component-model/ocm/pkg/contexts/credentials" @@ -41,6 +42,7 @@ type Plugin interface { SetShort(s string) SetLong(s string) + SetConfigParser(config func(raw json.RawMessage) (interface{}, error)) RegisterDownloader(arttype, mediatype string, u Downloader) error GetDownloader(name string) Downloader @@ -55,7 +57,8 @@ type Plugin interface { DecodeAccessSpecification(data []byte) (AccessSpec, error) GetAccessMethod(name string, version string) AccessMethod - Options() *Options + GetOptions() *Options + GetConfig() (interface{}, error) } type AccessMethod interface { diff --git a/pkg/contexts/ocm/plugin/ppi/plugin.go b/pkg/contexts/ocm/plugin/ppi/plugin.go index 2902ea6d6..e33a2900b 100644 --- a/pkg/contexts/ocm/plugin/ppi/plugin.go +++ b/pkg/contexts/ocm/plugin/ppi/plugin.go @@ -5,6 +5,7 @@ package ppi import ( + "encoding/json" "fmt" "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/options" @@ -29,6 +30,8 @@ type plugin struct { methods map[string]AccessMethod accessScheme runtime.Scheme + + configParser func(message json.RawMessage) (interface{}, error) } func NewPlugin(name string, version string) Plugin { @@ -66,7 +69,7 @@ func (p *plugin) Descriptor() internal.Descriptor { return p.descriptor } -func (p *plugin) Options() *Options { +func (p *plugin) GetOptions() *Options { return &p.options } @@ -78,6 +81,10 @@ func (p *plugin) SetShort(s string) { p.descriptor.Short = s } +func (p *plugin) SetConfigParser(config func(raw json.RawMessage) (interface{}, error)) { + p.configParser = config +} + func (p *plugin) RegisterDownloader(arttype, mediatype string, hdlr Downloader) error { key := DownloaderKey{}.SetArtefact(arttype, mediatype) if !key.IsValid() { @@ -232,7 +239,7 @@ func (p *plugin) RegisterAccessMethod(m AccessMethod) error { optlist = append(optlist, CLIOption{ Name: o.GetName(), Type: o.ValueType(), - Description: o.GetDescription(), + Description: o.GetDescriptionText(), }) } } @@ -242,13 +249,11 @@ func (p *plugin) RegisterAccessMethod(m AccessMethod) error { Name: m.Name(), Description: m.Description(), Format: m.Format(), - CLIOptions: optlist, } p.descriptor.AccessMethods = append(p.descriptor.AccessMethods, meth) p.accessScheme.RegisterByDecoder(m.Name(), m) p.methods[m.Name()] = m vers = "v1" - optlist = nil } meth := internal.AccessMethodDescriptor{ Name: m.Name(), @@ -278,3 +283,17 @@ func (p *plugin) GetAccessMethod(name string, version string) AccessMethod { } return p.methods[n] } + +func (p *plugin) GetConfig() (interface{}, error) { + if len(p.options.Config) == 0 { + return nil, nil + } + if p.configParser == nil { + var cfg interface{} + if err := json.Unmarshal(p.options.Config, &cfg); err != nil { + return nil, err + } + return &cfg, nil + } + return p.configParser(p.options.Config) +} diff --git a/pkg/contexts/ocm/registration/downloader.go b/pkg/contexts/ocm/registration/downloader.go new file mode 100644 index 000000000..45d58b4a6 --- /dev/null +++ b/pkg/contexts/ocm/registration/downloader.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + ocmcpi "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/download" + "github.com/open-component-model/ocm/pkg/contexts/ocm/internal" + "github.com/open-component-model/ocm/pkg/errors" +) + +func RegisterDownloadHandler(ctx internal.Context, hdlr download.Handler, olist ...BlobHandlerOption) error { + opts := ocmcpi.NewBlobHandlerOptions(olist...) + if opts.Priority > 0 { + return errors.ErrInvalid("option", "priority") + } + download.For(ctx).Register(opts.ArtefactType, opts.MimeType, hdlr) + return nil +} diff --git a/pkg/contexts/ocm/registration/registration.go b/pkg/contexts/ocm/registration/registration.go new file mode 100644 index 000000000..0c7d8ef42 --- /dev/null +++ b/pkg/contexts/ocm/registration/registration.go @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "github.com/mandelsoft/logging" + + "github.com/open-component-model/ocm/pkg/contexts/ocm" + access "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/plugin" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/blobhandler/generic/plugin" + "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "github.com/open-component-model/ocm/pkg/runtime" +) + +var TAG = logging.NewTag("plugins") + +// RegisterExtensions registers all the extension provided the found plugin +// at the given context. If no context is given, the cache context is used. +func RegisterExtensions(ctx ocm.Context) error { + pi := plugincacheattr.Get(ctx) + + for _, n := range pi.PluginNames() { + p := pi.Get(n) + if !p.IsValid() { + continue + } + for _, m := range p.GetDescriptor().AccessMethods { + name := m.Name + if m.Version != "" { + name = name + runtime.VersionSeparator + m.Version + } + ctx.Logger(TAG).Info("registering access method", + "plugin", p.Name(), + "type", name) + pi.GetContext().AccessMethods().Register(name, access.NewType(name, p, &m)) + } + + for _, u := range p.GetDescriptor().Uploaders { + for _, c := range u.Constraints { + if c.ContextType != "" && c.RepositoryType != "" && c.MediaType != "" { + hdlr, err := plugin.New(p, u.Name, nil) + if err != nil { + ctx.Logger(TAG).Error("cannot create blob handler fpr plugin", "plugin", p.Name(), "handler", u.Name) + } else { + ctx.Logger(TAG).Info("registering repository blob handler", + "context", c.ContextType+":"+c.RepositoryType, + "plugin", p.Name(), + "handler", u.Name) + ctx.BlobHandlers().Register(hdlr, cpi.ForRepo(c.ContextType, c.RepositoryType), cpi.ForMimeType(c.MediaType)) + } + } + } + } + } + return nil +} diff --git a/pkg/contexts/ocm/registration/uploader.go b/pkg/contexts/ocm/registration/uploader.go new file mode 100644 index 000000000..09ad0dbef --- /dev/null +++ b/pkg/contexts/ocm/registration/uploader.go @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package registration + +import ( + "github.com/open-component-model/ocm/pkg/contexts/ocm/internal" +) + +type ( + BlobHandlerOption = internal.BlobHandlerOption + BlobHandlerConfig = internal.BlobHandlerConfig + BlobHandlerOptions = internal.BlobHandlerOptions +) + +func RegisterBlobHandlerByName(ctx internal.Context, name string, config BlobHandlerConfig, opts ...BlobHandlerOption) error { + _, err := ctx.BlobHandlers().RegisterByName(name, ctx, config, opts...) + return err +} + +func WithPrio(prio int) BlobHandlerOption { + return internal.WithPrio(prio) +} + +func ForArtefactType(t string) BlobHandlerOption { + return internal.ForArtefactType(t) +} + +func ForMimeType(t string) BlobHandlerOption { + return internal.ForMimeType(t) +} diff --git a/pkg/contexts/ocm/resourcetypes/const.go b/pkg/contexts/ocm/resourcetypes/const.go index 16caef163..3bbc49ece 100644 --- a/pkg/contexts/ocm/resourcetypes/const.go +++ b/pkg/contexts/ocm/resourcetypes/const.go @@ -17,4 +17,8 @@ const ( BLOB = "blob" // FILESYSTEM describes a directory structure stored as archive (tar, tgz). FILESYSTEM = "filesystem" + // EXECUTABLE describes an OS executable. + EXECUTABLE = "executable" + // OCM_PLUGIN describes an OS executable OCM plugin. + OCM_PLUGIN = "ocmPlugin" ) diff --git a/pkg/contexts/ocm/session.go b/pkg/contexts/ocm/session.go index f7a2d8421..ff97b1337 100644 --- a/pkg/contexts/ocm/session.go +++ b/pkg/contexts/ocm/session.go @@ -67,6 +67,22 @@ func newSession(s datacontext.SessionBase) datacontext.Session { } } +type Finalizer interface { + Finalize() error +} + +type finalizer struct { + finalizer Finalizer +} + +func (f *finalizer) Close() error { + return f.finalizer.Finalize() +} + +func (s *session) Finalize(f Finalizer) { + s.Session.AddCloser(&finalizer{f}) +} + func (s *session) Close() error { return s.Session.Close() // TODO: cleanup cache diff --git a/pkg/contexts/ocm/usage.go b/pkg/contexts/ocm/usage.go index ab3466130..f3317ebbd 100644 --- a/pkg/contexts/ocm/usage.go +++ b/pkg/contexts/ocm/usage.go @@ -14,12 +14,13 @@ import ( func AccessUsage(scheme AccessTypeScheme, cli bool) string { s := ` -The following access methods are known by the system. +The following list describes the supported access methods, their versions +and specification formats. Typically there is special support for the CLI artifact add commands. -The following types (with the field type in the access field -are handled: +The access method specification can be put below the access field. +If always requires the field type describing the kind and version +shown below. ` - versions := map[string]map[string]string{} descs := map[string]string{} diff --git a/pkg/contexts/ocm/utils/registry/registry.go b/pkg/contexts/ocm/utils/registry/registry.go index a2c4426e1..ab7ac03a2 100644 --- a/pkg/contexts/ocm/utils/registry/registry.go +++ b/pkg/contexts/ocm/utils/registry/registry.go @@ -6,6 +6,9 @@ package registry import ( "strings" + + "github.com/open-component-model/ocm/pkg/generics" + "github.com/open-component-model/ocm/pkg/mime" ) type Key[K any] interface { @@ -59,13 +62,38 @@ func (p *Registry[H, K]) LookupHandler(key K) []H { mediatype := key.GetMediaType() arttype := key.GetArtefactType() - if mediatype == "" || arttype == "" { + if h := p.mappings[key.SetArtefact(arttype, "")]; len(h) > 0 { return h } - if h := p.mappings[key.SetArtefact(key.GetArtefactType(), "")]; len(h) > 0 { - return h + return p.lookupMedia(key.SetArtefact("", mediatype)) +} + +func (p *Registry[H, K]) LookupKeys(key K) generics.Set[K] { + found := generics.Set[K]{} + + if len(p.LookupHandler(key)) > 0 { + found.Add(key) + } + if key.GetArtefactType() == "" { + for k := range p.mappings { + if k.GetArtefactType() != "" { + c := k.SetArtefact(k.GetArtefactType(), key.GetMediaType()) + if !found.Contains(c) && len(p.LookupHandler(c)) > 0 { + found.Add(c) + } + } + } + } else { + for k := range p.mappings { + if mime.IsMoreGeneral(key.GetMediaType(), k.GetMediaType()) { + c := k.SetArtefact(key.GetArtefactType(), k.GetMediaType()) + if !found.Contains(c) && len(p.LookupHandler(c)) > 0 { + found.Add(c) + } + } + } } - return p.lookupMedia(key.SetArtefact("", key.GetMediaType())) + return found } func (p *Registry[H, K]) Register(key K, h H) { diff --git a/pkg/contexts/ocm/utils/registry/registry_test.go b/pkg/contexts/ocm/utils/registry/registry_test.go index ebd2bc917..106b3e108 100644 --- a/pkg/contexts/ocm/utils/registry/registry_test.go +++ b/pkg/contexts/ocm/utils/registry/registry_test.go @@ -9,9 +9,14 @@ import ( . "github.com/onsi/gomega" "github.com/open-component-model/ocm/pkg/contexts/ocm/utils/registry" + "github.com/open-component-model/ocm/pkg/generics" ) -var amMey = registry.RegistrationKey{}.SetArtefact("a", "m") +var aKey = registry.RegistrationKey{}.SetArtefact("a", "") +var mKey = registry.RegistrationKey{}.SetArtefact("", "m") +var amKey = registry.RegistrationKey{}.SetArtefact("a", "m") +var a1mKey = registry.RegistrationKey{}.SetArtefact("a1", "m") +var am1Key = registry.RegistrationKey{}.SetArtefact("a", "m1") var amtarKey = registry.RegistrationKey{}.SetArtefact("a", "m+tar") var _ = Describe("lookup", func() { @@ -21,57 +26,87 @@ var _ = Describe("lookup", func() { reg = registry.NewRegistry[string, registry.RegistrationKey]() }) - It("looks up complete", func() { - reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m"), "test") - reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") - reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") - - h := reg.LookupHandler(amMey) - Expect(h).To(Equal([]string{"test"})) - }) - - It("looks up partial artifact", func() { - reg.Register(registry.RegistrationKey{}.SetArtefact("a", ""), "test") - reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") - reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") - - h := reg.LookupHandler(amMey) - Expect(h).To(Equal([]string{"test"})) - }) - - It("looks up partial media", func() { - reg.Register(registry.RegistrationKey{}.SetArtefact("", "m"), "test") - reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") - reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") - - h := reg.LookupHandler(amMey) - Expect(h).To(Equal([]string{"test"})) + Context("lookup handler", func() { + It("looks up complete", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m"), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + h := reg.LookupHandler(amKey) + Expect(h).To(Equal([]string{"test"})) + }) + + It("looks up partial artifact", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("a", ""), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + h := reg.LookupHandler(amKey) + Expect(h).To(Equal([]string{"test"})) + }) + + It("looks up partial media", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("", "m"), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + h := reg.LookupHandler(amKey) + Expect(h).To(Equal([]string{"test"})) + }) + + It("looks complete with media sub type", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m"), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + h := reg.LookupHandler(amtarKey) + Expect(h).To(Equal([]string{"test"})) + }) + + It("looks partial with media sub type", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("", "m"), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + h := reg.LookupHandler(amtarKey) + Expect(h).To(Equal([]string{"test"})) + }) + + It("prefers art", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("", "m"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", ""), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + h := reg.LookupHandler(amtarKey) + Expect(h).To(Equal([]string{"test"})) + }) }) - It("looks complete with media sub type", func() { - reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m"), "test") - reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") - reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") - - h := reg.LookupHandler(amtarKey) - Expect(h).To(Equal([]string{"test"})) - }) - - It("looks partial with media sub type", func() { - reg.Register(registry.RegistrationKey{}.SetArtefact("", "m"), "test") - reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") - reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") - - h := reg.LookupHandler(amtarKey) - Expect(h).To(Equal([]string{"test"})) - }) - - It("prefers art", func() { - reg.Register(registry.RegistrationKey{}.SetArtefact("", "m"), "testm") - reg.Register(registry.RegistrationKey{}.SetArtefact("a", ""), "test") - reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") - - h := reg.LookupHandler(amtarKey) - Expect(h).To(Equal([]string{"test"})) + Context("lookup keys", func() { + It("fills missing", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m"), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + keys := reg.LookupKeys(aKey) + Expect(keys).To(Equal(generics.NewSet(amKey, am1Key))) + }) + + It("fills missing", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m+tar"), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + keys := reg.LookupKeys(mKey) + Expect(keys).To(Equal(generics.NewSet(a1mKey))) + }) + It("fills more specific media", func() { + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m+tar"), "test") + reg.Register(registry.RegistrationKey{}.SetArtefact("a", "m1"), "testm") + reg.Register(registry.RegistrationKey{}.SetArtefact("a1", "m"), "testa") + + keys := reg.LookupKeys(amKey) + Expect(keys).To(Equal(generics.NewSet(amtarKey))) + }) }) }) diff --git a/pkg/generics/set.go b/pkg/generics/set.go new file mode 100644 index 000000000..5ef815c00 --- /dev/null +++ b/pkg/generics/set.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package generics + +type Set[K comparable] map[K]struct{} + +func NewSet[K comparable](keys ...K) Set[K] { + return Set[K]{}.Add(keys...) +} + +func (s Set[K]) Add(keys ...K) Set[K] { + for _, k := range keys { + s[k] = struct{}{} + } + return s +} + +func (s Set[K]) Delete(keys ...K) Set[K] { + for _, k := range keys { + delete(s, k) + } + return s +} + +func (s Set[K]) Contains(keys ...K) bool { + for _, k := range keys { + if _, ok := s[k]; !ok { + return false + } + } + return true +} diff --git a/pkg/mime/util.go b/pkg/mime/util.go index cb58cbbf0..133e7706f 100644 --- a/pkg/mime/util.go +++ b/pkg/mime/util.go @@ -39,3 +39,19 @@ func BaseType(mime string) string { func IsGZip(mime string) bool { return strings.HasSuffix(mime, "/gzip") || strings.HasSuffix(mime, "+gzip") } + +func IsMoreGeneral(m string, specific string) bool { + if m == "" { + return true + } + for { + if m == specific { + return true + } + i := strings.LastIndex(specific, "+") + if i < 0 { + return false + } + specific = specific[:i] + } +} diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index 96af991c2..80f89353d 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -195,6 +195,12 @@ type defaultScheme struct { types KnownTypes } +type BaseScheme interface { + BaseScheme() Scheme +} + +var _ BaseScheme = (*defaultScheme)(nil) + func MustNewDefaultScheme(protoIfce interface{}, protoUnstr Unstructured, acceptUnknown bool, defaultdecoder TypedObjectDecoder, base ...Scheme) SchemeBase { return utils.Must(NewDefaultScheme(protoIfce, protoUnstr, acceptUnknown, defaultdecoder, base...)) } @@ -235,6 +241,10 @@ func NewDefaultScheme(protoIfce interface{}, protoUnstr Unstructured, acceptUnkn }, nil } +func (d *defaultScheme) BaseScheme() Scheme { + return d.base +} + func (d *defaultScheme) AddKnownTypes(s Scheme) { d.lock.Lock() defer d.lock.Unlock() @@ -345,9 +355,10 @@ func (d *defaultScheme) Decode(data []byte, unmarshal Unmarshaler) (TypedObject, if d.defaultdecoder != nil { o, err := d.defaultdecoder.Decode(data, unmarshal) if err == nil { - return o, nil - } - if !errors.IsErrUnknownKind(err, errors.KIND_OBJECTTYPE) { + if o != nil { + return o, nil + } + } else if !errors.IsErrUnknownKind(err, errors.KIND_OBJECTTYPE) { return nil, err } } diff --git a/pkg/semverutils/suite_test.go b/pkg/semverutils/suite_test.go new file mode 100644 index 000000000..0bb7fabb2 --- /dev/null +++ b/pkg/semverutils/suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package semverutils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Semver Utilities") +} diff --git a/pkg/semverutils/utils.go b/pkg/semverutils/utils.go new file mode 100644 index 000000000..519317391 --- /dev/null +++ b/pkg/semverutils/utils.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package semverutils + +import ( + "fmt" + "sort" + + "github.com/Masterminds/semver/v3" + + "github.com/open-component-model/ocm/pkg/errors" +) + +// MatchVersionStrings returns an ordered list of versions filtered by the given +// constraints. If no constraints a given the complete list is returned. +// If one given version is no semver version it is ignored for the matching +// and an additional error describing the parsing errors is returned. +func MatchVersionStrings(vers []string, constraints ...*semver.Constraints) (semver.Collection, error) { + var versions semver.Collection + list := errors.ErrListf("invalid semver versions") + for _, vn := range vers { + v, err := semver.NewVersion(vn) + if err == nil { + versions = append(versions, v) + } else { + list.Add(fmt.Errorf("%s", vn)) + } + } + return MatchVersions(versions, constraints...), list.Result() +} + +func MatchVersions(versions semver.Collection, constraints ...*semver.Constraints) semver.Collection { + // Filter by patterns + if len(constraints) > 0 { + next: + for i := 0; i < len(versions); i++ { + for _, c := range constraints { + if c.Check(versions[i]) { + continue next + } + } + versions = append(versions[:i], versions[i+1:]...) + i-- + } + } + sort.Sort(versions) + return versions +} diff --git a/pkg/semverutils/utils_test.go b/pkg/semverutils/utils_test.go new file mode 100644 index 000000000..0a60bed04 --- /dev/null +++ b/pkg/semverutils/utils_test.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package semverutils + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/testutils" + + "github.com/Masterminds/semver/v3" +) + +var _ = Describe("filter", func() { + V10, _ := semver.NewVersion("1.0") + V19, _ := semver.NewVersion("1.9") + V20, _ := semver.NewVersion("2.0") + V21, _ := semver.NewVersion("2.0.1") + + C2x, _ := semver.NewConstraint(">=2.0") + C19, _ := semver.NewConstraint("1.9") + + It("just sorts", func() { + Expect(Must(MatchVersionStrings([]string{"2.0", "1.0", "1.9"}))).To(Equal(semver.Collection{V10, V19, V20})) + }) + + It("filters by constraints", func() { + Expect(Must(MatchVersionStrings([]string{"2.0", "1.0", "2.0.1", "1.9"}, C2x))).To(Equal(semver.Collection{V20, V21})) + }) + + It("filters by multiple constraints", func() { + Expect(Must(MatchVersionStrings([]string{"2.0", "1.0", "1.9"}, C2x, C19))).To(Equal(semver.Collection{V19, V20})) + }) + + It("filters invalid", func() { + r, err := MatchVersionStrings([]string{"2.0", "1.0", "1.x", "1.9"}) + MustFailWithMessage(err, "invalid semver versions: 1.x") + Expect(r).To(Equal(semver.Collection{V10, V19, V20})) + }) + +}) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 76fa44c44..a78444c21 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -298,3 +298,6 @@ func Must[T any](o T, err error) T { } return o } + +func IgnoreError(_ error) { +}