From 5de0297cb8827b34810d94b7b367630d995fc63d Mon Sep 17 00:00:00 2001 From: Uwe Krueger Date: Tue, 25 Jun 2024 15:17:54 +0200 Subject: [PATCH] Support for CLI Extensions by OCM Plugins (#815) #### What this PR does / why we need it: The OCM Plugin framework now supports two new features: - definition of configuration types (consumed by the plugin) - definition of CLI commands (for the OCM CLI) Additionally it is possible to consume logging configuration from the OCM CLI for all plugin feature commands. Examples see coding in `cmds/cliplugin` #### Config Types Config types are just registered at the Plugin Object; ``` p := ppi.NewPlugin("cliplugin", version.Get().String()) ... p.RegisterConfigType(configType) ``` The argument is just the config type as registered at the ocm library, for example: ``` const ConfigType = "rhabarber.config.acme.org" type Config struct { runtime.ObjectVersionedType `json:",inline"` ... } func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error { ... } func init() { configType = cfgcpi.NewConfigType[*Config](ConfigType, usage) cfgcpi.RegisterConfigType(configType) } ``` #### CLI Commands CLI commands are simple configured `cobra.Command` objects. They are registered at the plugin object with ``` cmd, err := clicmd.NewCLICommand(NewCommand(), clicmd.WithCLIConfig(), clicmd.WithVerb("check")) if err != nil { os.Exit(1) } p.RegisterCommand(NewCommand()) ``` with coding similar to ``` type command struct { date string } func NewCommand() *cobra.Command { cmd := &command{} c := &cobra.Command{ Use: Name + " ", Short: "determine whether we are in rhubarb season", Long: "The rhubarb season is between march and april.", RunE: cmd.Run, } c.Flags().StringVarP(&cmd.date, "date", "d", "", "the date to ask for (MM/DD)") return c } func (c *command) Run(cmd *cobra.Command, args []string) error { ... } ``` The plugin programming interface supports the generation of an extension command directly from a cobra command object using the method `NewCLICommand` from the `ppi.clicmd` package. Otherwise the `ppi.Command` interface can be implemented without requiring a cobra command.. If the code wants to use the config framework, for example to - use the OCM library again - access credentials - get configured with declared config types the appropriate command feature must be set. For the cobra support this is implemented by the option `WithCLIConfig`. If set to true, the OCM CLI configuration is available for the config context used in the CLI code. The command can be a top-level command or attached to a dedicated verb (and optionally a realm like `ocm`or `oci`). For the cobra support this can be requested by the option `WithVerb(...)`. If the config framework is used just add the following anonymoud import for an automated configuration: ``` import ( // enable mandelsoft plugin logging configuration. _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/config" ) ``` The plugin code is then configured with the configuration of the OCM CLI and the config framework can be used. If the configuration should be handled by explicit plugin code a handler can be registered with ``` func init() { command.RegisterCommandConfigHandler(yourHandler) } ``` It gets a config yaml according to the config objects used by the OCM library. #### Logging To get the logging configuration from the OCM CLI the plugin has be configured with ``` p.ForwardLogging() ``` If the standard mandelsoft logging from the OCM library is used the configuration can be implemented directly with an anonymous import of ``` import ( // enable mandelsoft plugin logging configuration. _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/logging" ) ``` The plugin code is then configured with the logging configuration of the OCM CLI and the mandelsoft logging frame work can be used. If the logging configuration should be handled by explicit plugin code a handler can be registered with ``` func init() { cmds.RegisterLoggingConfigHandler(yourHandler) } ``` It gets a logging configuration yaml according to the logging config used by the OCM library (`github.com/mandelsoft/logging/config`). #### Using Plugin command extensions from the OCM library. The plugin command extensions can also be called without the OCM CLI directly from the OCM library. Therefore the plugin objects provided by the library can be used. Logging information and config information must explicitly be configured to be passed to the plugin. Therefore the context attribute `clicfgattr.Get(ctx)` is used. It can be set via `clicfgattr.Set(...)`. The logging configuration is extracted from the configured configuration object with target type `*logging.LoggingConfiguration`. If the code uses an OCM context configured with a `(ocm)utils.ConfigureXXX` function, the cli config attribute is set accordingly. #### Which issue(s) this PR fixes: --------- Co-authored-by: Fabian Burth Co-authored-by: Hilmar Falkenberg --- .golangci.yaml | 3 + Makefile | 11 +- cmds/cliplugin/cmds/check/cmd.go | 110 +++++++++ cmds/cliplugin/cmds/check/config.go | 70 ++++++ cmds/cliplugin/cmds/cmd_test.go | 154 ++++++++++++ cmds/cliplugin/cmds/suite_test.go | 13 + cmds/cliplugin/cmds/testdata/config.yaml | 3 + cmds/cliplugin/cmds/testdata/err.yaml | 3 + .../cliplugin/cmds/testdata/globallogcfg.yaml | 27 ++ cmds/cliplugin/cmds/testdata/logcfg.yaml | 13 + .../cliplugin/cmds/testdata/plugins/cliplugin | 2 + cmds/cliplugin/main.go | 35 +++ cmds/cliplugin/testdata/config.yaml | 3 + cmds/demoplugin/valuesets/check_test.go | 50 +--- cmds/ocm/app/app.go | 200 ++++++--------- cmds/ocm/app/app_test.go | 15 +- cmds/ocm/app/prepare.go | 7 +- cmds/ocm/clippi/config/evaluated.go | 13 + cmds/ocm/clippi/config/type.go | 232 ++++++++++++++++++ .../common/options/keyoption/config.go | 119 +++++++++ .../common/options/keyoption/option.go | 111 +-------- cmds/ocm/commands/misccmds/config/cmd.go | 21 ++ cmds/ocm/commands/misccmds/config/get/cmd.go | 77 ++++++ .../commands/misccmds/config/get/cmd_test.go | 47 ++++ .../misccmds/config/get/suite_test.go | 13 + cmds/ocm/commands/misccmds/names/names.go | 1 + .../common/handlers/comphdlr/typehandler.go | 3 + .../common/options/signoption/option.go | 2 +- .../ocmcmds/plugins/describe/cmd_test.go | 20 +- .../ocmcmds/plugins/describe/describe.go | 4 +- cmds/ocm/commands/ocmcmds/plugins/get/cmd.go | 30 +-- .../commands/ocmcmds/plugins/get/cmd_test.go | 19 +- .../plugins/tests/accessmethods/cmd_test.go | 8 +- .../plugins/tests/routingslips/cmd_test.go | 7 +- cmds/ocm/commands/plugin/cmd.go | 63 +++++ cmds/ocm/commands/verbs/get/cmd.go | 2 + cmds/ocm/commands/verbs/verb.go | 16 ++ cmds/ocm/pkg/output/elementoutput.go | 46 +++- cmds/ocm/pkg/output/output.go | 45 +++- cmds/subcmdplugin/cmds/cmd_test.go | 122 +++++++++ cmds/subcmdplugin/cmds/demo/cmd.go | 30 +++ cmds/subcmdplugin/cmds/group/cmd.go | 20 ++ cmds/subcmdplugin/cmds/suite_test.go | 13 + .../cmds/testdata/plugins/cliplugin | 2 + cmds/subcmdplugin/main.go | 33 +++ docs/command-plugins.md | 160 ++++++++++++ docs/pluginreference/plugin.md | 6 +- docs/pluginreference/plugin_command.md | 19 ++ docs/reference/ocm.md | 9 +- docs/reference/ocm_attributes.md | 8 +- docs/reference/ocm_configfile.md | 10 + docs/reference/ocm_get.md | 1 + docs/reference/ocm_get_config.md | 45 ++++ go.mod | 5 +- go.sum | 10 +- pkg/cobrautils/funcs.go | 51 ++++ pkg/cobrautils/logopts/close_test.go | 55 +++++ pkg/cobrautils/logopts/config.go | 146 +++++++++++ pkg/cobrautils/logopts/doc.go | 4 + pkg/cobrautils/logopts/logging/config.go | 61 +++++ pkg/cobrautils/logopts/logging/global.go | 6 + pkg/cobrautils/logopts/logging/logfiles.go | 91 +++++++ pkg/cobrautils/logopts/options.go | 125 ++-------- pkg/cobrautils/logopts/options_test.go | 27 +- pkg/cobrautils/template.go | 18 +- pkg/cobrautils/tweak.go | 66 ++++- pkg/cobrautils/utils.go | 14 ++ pkg/contexts/config/config/utils.go | 60 +++++ pkg/contexts/config/cpi/interface.go | 4 +- pkg/contexts/config/internal/configtypes.go | 2 + pkg/contexts/config/internal/context.go | 56 ++++- pkg/contexts/config/internal/store.go | 9 +- pkg/contexts/config/internal/updater.go | 8 +- pkg/contexts/config/plugin/type.go | 21 ++ .../repositories/dockerconfig/default.go | 10 +- .../credentials/repositories/npm/default.go | 14 +- .../datacontext/attrs/clicfgattr/attr.go | 70 ++++++ .../datacontext/attrs/logforward/attr.go | 2 +- .../datacontext/attrs/rootcertsattr/attr.go | 2 +- .../datacontext/attrs/vfsattr/attr.go | 2 +- .../datacontext/config/logging/type.go | 25 +- pkg/contexts/oci/testhelper/manifests.go | 5 +- .../ocm/accessmethods/plugin/cmd_test.go | 11 +- .../ocm/accessmethods/plugin/method_test.go | 8 +- .../ocm/accessmethods/plugin/testdata/test | 2 +- .../ocm/actionhandler/plugin/action_test.go | 8 +- .../generic/maven/blobhandler_test.go | 4 + .../handlers/generic/plugin/upload_test.go | 14 +- .../download/handlers/plugin/download_test.go | 8 +- .../routingslip/types/plugin/cmd_test.go | 6 +- .../routingslip/types/plugin/entry_test.go | 8 +- pkg/contexts/ocm/plugin/cache/plugin.go | 4 +- pkg/contexts/ocm/plugin/cache/plugindir.go | 84 ++++++- pkg/contexts/ocm/plugin/cache/updater.go | 126 +++++++--- pkg/contexts/ocm/plugin/common/describe.go | 128 ++++++++++ .../ocm/plugin/descriptor/descriptor.go | 75 ++++-- pkg/contexts/ocm/plugin/interface.go | 1 + pkg/contexts/ocm/plugin/plugin.go | 129 +++++++++- pkg/contexts/ocm/plugin/plugin_test.go | 11 +- pkg/contexts/ocm/plugin/ppi/clicmd/options.go | 75 ++++++ pkg/contexts/ocm/plugin/ppi/clicmd/utils.go | 85 +++++++ pkg/contexts/ocm/plugin/ppi/cmds/app.go | 19 +- .../ocm/plugin/ppi/cmds/command/cmd.go | 66 +++++ .../ocm/plugin/ppi/cmds/command/config.go | 19 ++ .../ocm/plugin/ppi/cmds/common/const.go | 11 +- pkg/contexts/ocm/plugin/ppi/cmds/logging.go | 29 +++ pkg/contexts/ocm/plugin/ppi/config/config.go | 28 +++ pkg/contexts/ocm/plugin/ppi/config/doc.go | 6 + pkg/contexts/ocm/plugin/ppi/interface.go | 40 +++ pkg/contexts/ocm/plugin/ppi/logging/config.go | 25 ++ pkg/contexts/ocm/plugin/ppi/logging/doc.go | 6 + pkg/contexts/ocm/plugin/ppi/options.go | 11 +- pkg/contexts/ocm/plugin/ppi/plugin.go | 199 ++++++++++++--- .../ocm/plugin/testutils/plugintests.go | 31 +++ pkg/contexts/ocm/registration/registration.go | 14 ++ pkg/contexts/ocm/signing/signing_test.go | 10 +- pkg/contexts/ocm/testhelper/resources.go | 5 +- pkg/contexts/ocm/utils/configure.go | 59 ++++- .../utils/defaultconfigregistry/configure.go | 2 +- .../handlers/plugin/handler_test.go | 6 +- pkg/env/env.go | 26 +- pkg/filelock/lock.go | 148 +++++++++++ pkg/filelock/lock_test.go | 36 +++ pkg/filelock/suite_test.go | 13 + pkg/filelock/testdata/lock | 0 pkg/logging/logging.go | 25 +- 126 files changed, 3978 insertions(+), 708 deletions(-) create mode 100644 cmds/cliplugin/cmds/check/cmd.go create mode 100644 cmds/cliplugin/cmds/check/config.go create mode 100644 cmds/cliplugin/cmds/cmd_test.go create mode 100644 cmds/cliplugin/cmds/suite_test.go create mode 100644 cmds/cliplugin/cmds/testdata/config.yaml create mode 100644 cmds/cliplugin/cmds/testdata/err.yaml create mode 100644 cmds/cliplugin/cmds/testdata/globallogcfg.yaml create mode 100644 cmds/cliplugin/cmds/testdata/logcfg.yaml create mode 100755 cmds/cliplugin/cmds/testdata/plugins/cliplugin create mode 100644 cmds/cliplugin/main.go create mode 100644 cmds/cliplugin/testdata/config.yaml create mode 100644 cmds/ocm/clippi/config/evaluated.go create mode 100644 cmds/ocm/clippi/config/type.go create mode 100644 cmds/ocm/commands/common/options/keyoption/config.go create mode 100644 cmds/ocm/commands/misccmds/config/cmd.go create mode 100644 cmds/ocm/commands/misccmds/config/get/cmd.go create mode 100644 cmds/ocm/commands/misccmds/config/get/cmd_test.go create mode 100644 cmds/ocm/commands/misccmds/config/get/suite_test.go create mode 100644 cmds/ocm/commands/plugin/cmd.go create mode 100644 cmds/ocm/commands/verbs/verb.go create mode 100644 cmds/subcmdplugin/cmds/cmd_test.go create mode 100644 cmds/subcmdplugin/cmds/demo/cmd.go create mode 100644 cmds/subcmdplugin/cmds/group/cmd.go create mode 100644 cmds/subcmdplugin/cmds/suite_test.go create mode 100755 cmds/subcmdplugin/cmds/testdata/plugins/cliplugin create mode 100644 cmds/subcmdplugin/main.go create mode 100644 docs/command-plugins.md create mode 100644 docs/pluginreference/plugin_command.md create mode 100644 docs/reference/ocm_get_config.md create mode 100644 pkg/cobrautils/logopts/close_test.go create mode 100644 pkg/cobrautils/logopts/config.go create mode 100644 pkg/cobrautils/logopts/doc.go create mode 100644 pkg/cobrautils/logopts/logging/config.go create mode 100644 pkg/cobrautils/logopts/logging/global.go create mode 100644 pkg/cobrautils/logopts/logging/logfiles.go create mode 100644 pkg/cobrautils/utils.go create mode 100644 pkg/contexts/config/config/utils.go create mode 100644 pkg/contexts/config/plugin/type.go create mode 100644 pkg/contexts/datacontext/attrs/clicfgattr/attr.go create mode 100644 pkg/contexts/ocm/plugin/ppi/clicmd/options.go create mode 100644 pkg/contexts/ocm/plugin/ppi/clicmd/utils.go create mode 100644 pkg/contexts/ocm/plugin/ppi/cmds/command/cmd.go create mode 100644 pkg/contexts/ocm/plugin/ppi/cmds/command/config.go create mode 100644 pkg/contexts/ocm/plugin/ppi/cmds/logging.go create mode 100644 pkg/contexts/ocm/plugin/ppi/config/config.go create mode 100644 pkg/contexts/ocm/plugin/ppi/config/doc.go create mode 100644 pkg/contexts/ocm/plugin/ppi/logging/config.go create mode 100644 pkg/contexts/ocm/plugin/ppi/logging/doc.go create mode 100644 pkg/contexts/ocm/plugin/testutils/plugintests.go create mode 100644 pkg/filelock/lock.go create mode 100644 pkg/filelock/lock_test.go create mode 100644 pkg/filelock/suite_test.go create mode 100644 pkg/filelock/testdata/lock diff --git a/.golangci.yaml b/.golangci.yaml index d4bf47be4f..e8aa23bd0e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -88,6 +88,9 @@ linters: - sqlclosecheck - wastedassign + # Disabled because of deprecation + - execinquery + linters-settings: gci: sections: diff --git a/Makefile b/Makefile index 4779168d14..e35074471f 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ COMMIT := $(shell git rev-parse --verify CONTROLLER_TOOLS_VERSION ?= v0.14.0 CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +PLATFORMS = windows/amd64 darwin/arm64 darwin/amd64 linux/amd64 linux/arm64 + CREDS ?= OCM := go run $(REPO_ROOT)/cmds/ocm $(CREDS) CTF_TYPE ?= tgz @@ -36,9 +38,16 @@ build: ${SOURCES} CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/ocm ./cmds/ocm CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/helminstaller ./cmds/helminstaller CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/demo ./cmds/demoplugin + CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/cliplugin ./cmds/cliplugin CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/ecrplugin ./cmds/ecrplugin +build-platforms: $(GEN)/.exists $(SOURCES) + @for i in $(PLATFORMS); do \ + echo GOARCH=$$(basename $$i) GOOS=$$(dirname $$i); \ + GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build ./cmds/ocm ./cmds/helminstaller ./cmds/ecrplugin; \ + done + .PHONY: install-requirements install-requirements: @make -C hack $@ @@ -46,7 +55,7 @@ install-requirements: .PHONY: prepare prepare: generate format generate-deepcopy build test check -EFFECTIVE_DIRECTORIES := $(REPO_ROOT)/cmds/ocm/... $(REPO_ROOT)/cmds/helminstaller/... $(REPO_ROOT)/cmds/ecrplugin/... $(REPO_ROOT)/cmds/demoplugin/... $(REPO_ROOT)/examples/... $(REPO_ROOT)/pkg/... +EFFECTIVE_DIRECTORIES := $(REPO_ROOT)/cmds/ocm/... $(REPO_ROOT)/cmds/helminstaller/... $(REPO_ROOT)/cmds/ecrplugin/... $(REPO_ROOT)/cmds/demoplugin/... $(REPO_ROOT)/cmds/cliplugin/... $(REPO_ROOT)/examples/... $(REPO_ROOT)/cmds/subcmdplugin/... $(REPO_ROOT)/pkg/... .PHONY: format format: diff --git a/cmds/cliplugin/cmds/check/cmd.go b/cmds/cliplugin/cmds/check/cmd.go new file mode 100644 index 0000000000..a56de59752 --- /dev/null +++ b/cmds/cliplugin/cmds/check/cmd.go @@ -0,0 +1,110 @@ +package check + +import ( + "fmt" + "strconv" + "strings" + "time" + + // bind OCM configuration. + _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/config" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/logging" + "github.com/spf13/cobra" + + "github.com/open-component-model/ocm/pkg/contexts/ocm" +) + +const Name = "check" + +var log = logging.DynamicLogger(logging.DefaultContext(), logging.NewRealm("cliplugin/rhabarber")) + +func New() *cobra.Command { + cmd := &command{} + c := &cobra.Command{ + Use: Name + " ", + Short: "determine whether we are in rhubarb season", + Long: "The rhubarb season is between march and april.", + RunE: cmd.Run, + } + + c.Flags().StringVarP(&cmd.date, "date", "d", "", "the date to ask for (MM/DD)") + return c +} + +type command struct { + date string +} + +var months = map[string]int{ + "jan": 1, + "feb": 2, + "mär": 3, "mar": 3, + "apr": 4, + "mai": 5, "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "okt": 10, "oct": 10, + "nov": 11, + "dez": 12, "dec": 12, +} + +func (c *command) Run(cmd *cobra.Command, args []string) error { + season := Season{ + Start: "mar/1", + End: "apr/30", + } + + ctx := ocm.FromContext(cmd.Context()) + ctx.ConfigContext().ApplyTo(0, &season) + + start, err := ParseDate(season.Start) + if err != nil { + return errors.Wrapf(err, "invalid season start") + } + + end, err := ParseDate(season.End) + if err != nil { + return errors.Wrapf(err, "invalid season end") + } + end = end.Add(time.Hour * 24) + + d := time.Now() + if c.date != "" { + d, err = ParseDate(c.date) + if err != nil { + return err + } + } + + log.Debug("testing rhabarb season", "date", d.String()) + if d.After(start) && d.Before(end) { + fmt.Printf("Yeah, it's rhabarb season - happy rhabarbing!\n") + } else { + fmt.Printf("Sorry, but you have to stay hungry.\n") + } + return nil +} + +func ParseDate(s string) (time.Time, error) { + parts := strings.Split(s, "/") + if len(parts) != 2 { + return time.Time{}, fmt.Errorf("invalid date, expected MM/DD") + } + month, err := strconv.Atoi(parts[0]) + if err != nil { + month = months[strings.ToLower(parts[0])] + if month == 0 { + return time.Time{}, errors.Wrapf(err, "invalid month") + } + } + day, err := strconv.Atoi(parts[1]) + if err != nil { + return time.Time{}, errors.Wrapf(err, "invalid day") + } + + return time.Date(time.Now().Year(), time.Month(month), day, 0, 0, 0, 0, time.Local), nil //nolint:gosmopolitan // yes +} diff --git a/cmds/cliplugin/cmds/check/config.go b/cmds/cliplugin/cmds/check/config.go new file mode 100644 index 0000000000..7caed81dae --- /dev/null +++ b/cmds/cliplugin/cmds/check/config.go @@ -0,0 +1,70 @@ +package check + +import ( + cfgcpi "github.com/open-component-model/ocm/pkg/contexts/config/cpi" + "github.com/open-component-model/ocm/pkg/runtime" +) + +const ( + ConfigType = "rhabarber.config.acme.org" + ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1" +) + +var ( + RhabarberType cfgcpi.ConfigType + RhabarberTypeV1 cfgcpi.ConfigType +) + +func init() { + RhabarberType = cfgcpi.NewConfigType[*Config](ConfigType, usage) + cfgcpi.RegisterConfigType(RhabarberType) + RhabarberTypeV1 = cfgcpi.NewConfigType[*Config](ConfigTypeV1, "") + + cfgcpi.RegisterConfigType(RhabarberTypeV1) +} + +type Season struct { + Start string `json:"start"` + End string `json:"end"` +} + +// Config describes a memory based repository interface. +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + Season `json:",inline"` +} + +// NewConfig creates a new memory ConfigSpec. +func NewConfig(start, end string) *Config { + return &Config{ + ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType), + Season: Season{ + Start: start, + End: end, + }, + } +} + +func (a *Config) GetType() string { + return ConfigType +} + +func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error { + t, ok := target.(*Season) + if !ok { + return cfgcpi.ErrNoContext(ConfigType) + } + + *t = a.Season + return nil +} + +const usage = ` +The config type ` + ConfigType + ` can be used to configure the season for rhubarb: + +
+    type: ` + ConfigType + `
+    start: mar/1
+    end: apr/30
+
+` diff --git a/cmds/cliplugin/cmds/cmd_test.go b/cmds/cliplugin/cmds/cmd_test.go new file mode 100644 index 0000000000..6799f19baa --- /dev/null +++ b/cmds/cliplugin/cmds/cmd_test.go @@ -0,0 +1,154 @@ +//go:build unix + +package cmds_test + +import ( + "bytes" + + . "github.com/mandelsoft/goutils/testutils" + . "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/ocm/plugin/testutils" + + "github.com/mandelsoft/logging/logrusl" + "github.com/mandelsoft/logging/utils" + + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" + "github.com/open-component-model/ocm/pkg/version" +) + +const KIND = "rhubarb" + +var _ = Describe("cliplugin", func() { + Context("lib", func() { + var env *TestEnv + var plugins TempPluginDir + + BeforeEach(func() { + env = NewTestEnv(TestData()) + plugins = Must(ConfigureTestPlugins(env, "testdata/plugins")) + + registry := plugincacheattr.Get(env) + // Expect(registration.RegisterExtensions(env)).To(Succeed()) + p := registry.Get("cliplugin") + Expect(p).NotTo(BeNil()) + Expect(p.Error()).To(Equal("")) + }) + + AfterEach(func() { + plugins.Cleanup() + env.Cleanup() + }) + + It("run plugin based ocm command", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("--config", "testdata/config.yaml", "check", KIND, "-d", "jul/10")) + + Expect("\n" + buf.String()).To(Equal(` +Yeah, it's rhabarb season - happy rhabarbing! +`)) + }) + + It("runs plugin based ocm command with log", func() { + var stdout bytes.Buffer + var stdlog bytes.Buffer + + lctx := env.OCMContext().LoggingContext() + lctx.SetBaseLogger(logrusl.WithWriter(utils.NewSyncWriter(&stdlog)).NewLogr()) + MustBeSuccessful(env.CatchOutput(&stdout). + Execute("--config", "testdata/logcfg.yaml", "check", KIND, "-d", "jul/10")) + + Expect("\n" + stdout.String()).To(Equal(` +Yeah, it's rhabarb season - happy rhabarbing! +`)) + // {"date":".*","level":"debug","msg":"testing rhabarb season","realm":"cliplugin/rhabarber","time":".*"} + Expect(stdlog.String()).To(StringMatchTrimmedWithContext(` +[^ ]* debug \[cliplugin/rhabarber\] "testing rhabarb season" date="[^"]*" +`)) + }) + + It("fails for undeclared config", func() { + var buf bytes.Buffer + + Expect(env.CatchOutput(&buf).Execute("--config", "testdata/err.yaml", "check", KIND, "-d", "jul/10")).To( + MatchError(`config type "err.config.acme.org" is unknown`)) + }) + + It("shows command help", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("check", KIND, "--help")) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +ocm check rhubarb — Determine Whether We Are In Rhubarb Season + +Synopsis: + ocm check rhubarb + +Flags: + -d, --date string the date to ask for (MM/DD) + -h, --help help for check + +Description: + The rhubarb season is between march and april. + +`)) + }) + + It("shows command help from main command", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("help", "check", KIND)) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +ocm check rhubarb — Determine Whether We Are In Rhubarb Season + +Synopsis: + ocm check rhubarb + +Flags: + -d, --date string the date to ask for (MM/DD) + -h, --help help for check + +Description: + The rhubarb season is between march and april. + +`)) + }) + + It("describe", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("describe", "plugin", "cliplugin")) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +Plugin Name: cliplugin +Plugin Version: ` + version.Get().String() + ` +Path: ` + plugins.Path() + `/cliplugin +Status: valid +Source: manually installed +Capabilities: CLI Commands, Config Types +Description: + The plugin offers the check command for object type rhubarb to check the rhubarb season. + +CLI Extensions: +- Name: check (determine whether we are in rhubarb season) + Object: rhubarb + Verb: check + Usage: check rhubarb + The rhubarb season is between march and april. + +Config Types for CLI Command Extensions: +- Name: rhabarber.config.acme.org + The config type «rhabarber.config.acme.org» can be used to configure the season for rhubarb: + + type: rhabarber.config.acme.org + start: mar/1 + end: apr/30 + + Versions: + - Version: v1 +*** found 1 plugins +`)) + }) + }) +}) diff --git a/cmds/cliplugin/cmds/suite_test.go b/cmds/cliplugin/cmds/suite_test.go new file mode 100644 index 0000000000..43716adc6c --- /dev/null +++ b/cmds/cliplugin/cmds/suite_test.go @@ -0,0 +1,13 @@ +package cmds_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Demo CLI Plugin Command Test Suite") +} diff --git a/cmds/cliplugin/cmds/testdata/config.yaml b/cmds/cliplugin/cmds/testdata/config.yaml new file mode 100644 index 0000000000..25763cf542 --- /dev/null +++ b/cmds/cliplugin/cmds/testdata/config.yaml @@ -0,0 +1,3 @@ +type: rhabarber.config.acme.org +start: jul/1 +end: jul/31 diff --git a/cmds/cliplugin/cmds/testdata/err.yaml b/cmds/cliplugin/cmds/testdata/err.yaml new file mode 100644 index 0000000000..cf1e7fcee6 --- /dev/null +++ b/cmds/cliplugin/cmds/testdata/err.yaml @@ -0,0 +1,3 @@ +type: err.config.acme.org +start: jul/1 +end: jul/31 diff --git a/cmds/cliplugin/cmds/testdata/globallogcfg.yaml b/cmds/cliplugin/cmds/testdata/globallogcfg.yaml new file mode 100644 index 0000000000..ff38f826f4 --- /dev/null +++ b/cmds/cliplugin/cmds/testdata/globallogcfg.yaml @@ -0,0 +1,27 @@ +{ + "type":"generic.config.ocm.software", + "configurations":[ + {"configurations":[ + { + "type": "logging.config.ocm.software/v1", + "contextType": "global", + "settings": { + "rules": [ + { + "rule": { + "level": "Debug", + "conditions": [ + { + "realmprefix": "cliplugin", + } + ] + } + } + ] + } + } + ], + "type":"generic.config.ocm.software" + } + ] +} diff --git a/cmds/cliplugin/cmds/testdata/logcfg.yaml b/cmds/cliplugin/cmds/testdata/logcfg.yaml new file mode 100644 index 0000000000..2eda89edc2 --- /dev/null +++ b/cmds/cliplugin/cmds/testdata/logcfg.yaml @@ -0,0 +1,13 @@ +type: generic.config.ocm.software +configurations: + - type: rhabarber.config.acme.org + start: jul/1 + end: jul/31 + - type: logging.config.ocm.software/v1 + contextType: "global" + settings: + rules: + - rule: + level: "Debug" + conditions: + - realmprefix: "cliplugin" diff --git a/cmds/cliplugin/cmds/testdata/plugins/cliplugin b/cmds/cliplugin/cmds/testdata/plugins/cliplugin new file mode 100755 index 0000000000..a29873a10f --- /dev/null +++ b/cmds/cliplugin/cmds/testdata/plugins/cliplugin @@ -0,0 +1,2 @@ +#!/bin/bash +go run ../main.go "$@" \ No newline at end of file diff --git a/cmds/cliplugin/main.go b/cmds/cliplugin/main.go new file mode 100644 index 0000000000..4a7f49006e --- /dev/null +++ b/cmds/cliplugin/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + + // enable mandelsoft plugin logging configuration. + _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/logging" + + "github.com/open-component-model/ocm/cmds/cliplugin/cmds/check" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/clicmd" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds" + "github.com/open-component-model/ocm/pkg/version" +) + +func main() { + p := ppi.NewPlugin("cliplugin", version.Get().String()) + + p.SetShort("Demo plugin with a simple cli extension") + p.SetLong("The plugin offers the check command for object type rhubarb to check the rhubarb season.") + + cmd, err := clicmd.NewCLICommand(check.New(), clicmd.WithCLIConfig(), clicmd.WithObjectType("rhubarb"), clicmd.WithVerb("check")) + if err != nil { + os.Exit(1) + } + p.RegisterCommand(cmd) + p.ForwardLogging() + + p.RegisterConfigType(check.RhabarberType) + p.RegisterConfigType(check.RhabarberTypeV1) + err = cmds.NewPluginCommand(p).Execute(os.Args[1:]) + if err != nil { + os.Exit(1) + } +} diff --git a/cmds/cliplugin/testdata/config.yaml b/cmds/cliplugin/testdata/config.yaml new file mode 100644 index 0000000000..25763cf542 --- /dev/null +++ b/cmds/cliplugin/testdata/config.yaml @@ -0,0 +1,3 @@ +type: rhabarber.config.acme.org +start: jul/1 +end: jul/31 diff --git a/cmds/demoplugin/valuesets/check_test.go b/cmds/demoplugin/valuesets/check_test.go index 9b76d00843..aaa601a62b 100644 --- a/cmds/demoplugin/valuesets/check_test.go +++ b/cmds/demoplugin/valuesets/check_test.go @@ -6,15 +6,14 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env" . "github.com/open-component-model/ocm/pkg/env/builder" "github.com/spf13/pflag" "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/labels/routingslip" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/composition" "github.com/open-component-model/ocm/pkg/signing/handlers/rsa" @@ -28,54 +27,14 @@ const ( ) var _ = Describe("demoplugin", func() { - /* - Context("cli", func() { - var env *testhelper.TestEnv - - BeforeEach(func() { - env = testhelper.NewTestEnv(testhelper.TestData()) - - cache.DirectoryCache.Reset() - plugindirattr.Set(env.OCMContext(), "testdata") - - env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { - env.ComponentVersion(COMP, VERS, func() { - env.Provider(PROV) - }) - }) - env.RSAKeyPair(PROV) - }) - - AfterEach(func() { - env.Cleanup() - }) - - It("add check routing slip entry", func() { - buf := bytes.NewBuffer(nil) - MustBeSuccessful(env.CatchOutput(buf).Execute("add", "routingslip", ARCH, PROV, "check", "--checkStatus", "test=passed", "--checkMessage", "test=25 tests successful")) - Expect(buf.String()).To(Equal("")) - - buf.Reset() - MustBeSuccessful(env.CatchOutput(buf).Execute("get", "routingslip", ARCH, PROV)) - Expect(buf.String()).To(StringMatchTrimmedWithContext(` - COMPONENT-VERSION NAME TYPE TIMESTAMP DESCRIPTION - acme.org/test:1.0.0 acme.org check .{20} test: passed - `)) - buf.Reset() - MustBeSuccessful(env.CatchOutput(buf).Execute("get", "routingslip", ARCH, PROV, "-oyaml")) - Expect(buf.String()).To(StringMatchTrimmedWithContext(`message: 25 tests successful`)) - }) - }) - */ - Context("lib", func() { var env *Builder + var plugins TempPluginDir BeforeEach(func() { - env = NewBuilder(TestData()) - cache.DirectoryCache.Reset() - plugindirattr.Set(env.OCMContext(), "testdata") + env = NewBuilder(TestData()) + plugins = Must(ConfigureTestPlugins(env, "testdata")) registry := plugincacheattr.Get(env) Expect(registration.RegisterExtensions(env)).To(Succeed()) @@ -92,6 +51,7 @@ var _ = Describe("demoplugin", func() { }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/cmds/ocm/app/app.go b/cmds/ocm/app/app.go index 93358c7cb1..43513dd939 100644 --- a/cmds/ocm/app/app.go +++ b/cmds/ocm/app/app.go @@ -10,11 +10,11 @@ import ( _ "github.com/open-component-model/ocm/pkg/contexts/clictx/config" _ "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs" - "github.com/mandelsoft/goutils/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" + config2 "github.com/open-component-model/ocm/cmds/ocm/clippi/config" "github.com/open-component-model/ocm/cmds/ocm/commands/cachecmds" "github.com/open-component-model/ocm/cmds/ocm/commands/common/options/keyoption" "github.com/open-component-model/ocm/cmds/ocm/commands/misccmds/action" @@ -28,7 +28,9 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/resources" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/routingslips" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/sources" + "github.com/open-component-model/ocm/cmds/ocm/commands/plugin" "github.com/open-component-model/ocm/cmds/ocm/commands/toicmds" + "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/add" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/bootstrap" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/check" @@ -58,36 +60,24 @@ import ( topicocmrefs "github.com/open-component-model/ocm/cmds/ocm/topics/ocm/refs" topicocmuploaders "github.com/open-component-model/ocm/cmds/ocm/topics/ocm/uploadhandlers" topicbootstrap "github.com/open-component-model/ocm/cmds/ocm/topics/toi/bootstrapping" - common2 "github.com/open-component-model/ocm/pkg/clisupport" "github.com/open-component-model/ocm/pkg/cobrautils" "github.com/open-component-model/ocm/pkg/cobrautils/logopts" - "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/credentials" - "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/signingattr" + "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/clicfgattr" + "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/contexts/ocm/utils/defaultconfigregistry" "github.com/open-component-model/ocm/pkg/out" - "github.com/open-component-model/ocm/pkg/signing" "github.com/open-component-model/ocm/pkg/version" ) type CLIOptions struct { - keyoption.Option - - Completed bool - Config []string - ConfigSets []string - Credentials []string - Context clictx.Context - Settings []string - Verbose bool - LogOpts logopts.Options - Version bool + config2.Config + Completed bool + Version bool + + *config2.EvaluatedOptions + Context clictx.Context } var desc = ` @@ -183,6 +173,7 @@ func NewCliCommandForArgs(ctx clictx.Context, args []string, mod ...func(clictx. if err != nil { return nil, err } + clicfgattr.Set(ctx.OCMContext(), opts.ConfigForward) cmd := newCliCommand(opts, mod...) cmd.SetArgs(args) return cmd, nil @@ -263,14 +254,9 @@ func newCliCommand(opts *CLIOptions, mod ...func(clictx.Context, *cobra.Command) cmd.AddCommand(cmdutils.OverviewCommand(creds.NewCommand(opts.Context))) opts.AddFlags(cmd.Flags()) - cmd.InitDefaultHelpCmd() - var help *cobra.Command - for _, c := range cmd.Commands() { - if c.Name() == "help" { - help = c - break - } - } + + help := cobrautils.TweakHelpCommandFor(cmd) + // help.Use="help " help.DisableFlagsInUseLine = true cmd.AddCommand(topicconfig.New(ctx)) @@ -297,6 +283,61 @@ func newCliCommand(opts *CLIOptions, mod ...func(clictx.Context, *cobra.Command) help.AddCommand(attributes.New(ctx)) help.AddCommand(topicbootstrap.New(ctx, "toi-bootstrapping")) + // register CLI extension commands + pi := plugincacheattr.Get(ctx) + + for _, n := range pi.PluginNames() { + p := pi.Get(n) + if !p.IsValid() { + continue + } + for _, c := range p.GetDescriptor().Commands { + if c.Verb != "" { + objtype := c.Name + if c.ObjectType != "" { + objtype = c.ObjectType + } + v := cobrautils.Find(cmd, c.Verb) + if v == nil { + v = verbs.NewCommand(ctx, c.Verb, "additional plugin based commands") + cmd.AddCommand(v) + } + s := cobrautils.Find(v, objtype) + if s != nil { + out.Errf(opts.Context, "duplicate cli command %q of plugin %q for verb %q", objtype, p.Name(), c.Verb) + } else { + v.AddCommand(plugin.NewCommand(ctx, p, c.Name, objtype)) + } + + if c.Realm != "" { + r := cobrautils.Find(cmd, c.Realm) + if r == nil { + out.Errf(opts.Context, "unknown realm %q for cli command %q of plugin %q", c.Realm, objtype, p.Name()) + } else { + v := cobrautils.Find(r, objtype) + if v == nil { + out.Errf(opts.Context, "unknown object %q for cli command %q of plugin %q", c.Realm, objtype, p.Name()) + } else { + s := cobrautils.Find(v, c.Verb) + if s != nil { + out.Errf(opts.Context, "duplicate cli command %q of plugin %q for realm %q verb %q", objtype, p.Name(), c.Realm, c.Verb) + } else { + v.AddCommand(plugin.NewCommand(ctx, p, c.Verb)) + } + } + } + } + } else { + s := cobrautils.Find(cmd, c.Name) + if s != nil { + out.Errf(opts.Context, "duplicate top-level cli command %q of plugin %q", c.Name, p.Name()) + } else { + cmd.AddCommand(plugin.NewCommand(ctx, p, c.Name)) + } + } + } + } + for _, m := range mod { if m != nil { m(ctx, cmd) @@ -306,15 +347,9 @@ func newCliCommand(opts *CLIOptions, mod ...func(clictx.Context, *cobra.Command) } func (o *CLIOptions) AddFlags(fs *pflag.FlagSet) { - fs.StringArrayVarP(&o.Config, "config", "", nil, "configuration file") - fs.StringSliceVarP(&o.ConfigSets, "config-set", "", nil, "apply configuration set") - fs.StringArrayVarP(&o.Credentials, "cred", "C", nil, "credential setting") - fs.StringArrayVarP(&o.Settings, "attribute", "X", nil, "attribute setting") - fs.BoolVarP(&o.Verbose, "verbose", "v", false, "deprecated: enable logrus verbose logging") - fs.BoolVarP(&o.Version, "version", "", false, "show version") // otherwise it is implicitly added by cobra + o.Config.AddFlags(fs) - o.LogOpts.AddFlags(fs) - o.Option.AddFlags(fs) + fs.BoolVarP(&o.Version, "version", "", false, "show version") // otherwise it is implicitly added by cobra } func (o *CLIOptions) Close() error { @@ -322,6 +357,8 @@ func (o *CLIOptions) Close() error { } func (o *CLIOptions) Complete() error { + var err error + if o.Completed { return nil } @@ -331,95 +368,18 @@ func (o *CLIOptions) Complete() error { logrus.SetLevel(logrus.DebugLevel) } - err := o.LogOpts.Configure(o.Context.OCMContext(), nil) - if err != nil { - return err - } + old := o.Context.ConfigContext().SkipUnknownConfig(true) + defer o.Context.ConfigContext().SkipUnknownConfig(old) - if len(o.Config) == 0 { - _, err = utils.Configure(o.Context.OCMContext(), "", vfsattr.Get(o.Context)) - if err != nil { - return err - } - } - for _, config := range o.Config { - _, err = utils.Configure(o.Context.OCMContext(), config, vfsattr.Get(o.Context)) - if err != nil { - return err - } - } - - err = o.Option.Configure(o.Context) + o.EvaluatedOptions, err = o.Config.Evaluate(o.Context.OCMContext(), true) if err != nil { return err } - - if o.Keys.HasKeys() { - def := signingattr.Get(o.Context.OCMContext()) - err = signingattr.Set(o.Context.OCMContext(), signing.NewRegistry(def.HandlerRegistry(), signing.NewKeyRegistry(o.Keys, def.KeyRegistry()))) - if err != nil { - return err - } - } - - for _, n := range o.ConfigSets { - err := o.Context.ConfigContext().ApplyConfigSet(n) - if err != nil { - return err - } - } - - id := credentials.ConsumerIdentity{} - attrs := common.Properties{} - for _, s := range o.Credentials { - i := strings.Index(s, "=") - if i < 0 { - return errors.ErrInvalid("credential setting", s) - } - name := s[:i] - value := s[i+1:] - if strings.HasPrefix(name, ":") { - if len(attrs) != 0 { - o.Context.CredentialsContext().SetCredentialsForConsumer(id, credentials.NewCredentials(attrs)) - id = credentials.ConsumerIdentity{} - attrs = common.Properties{} - } - name = name[1:] - id[name] = value - } else { - attrs[name] = value - } - if len(name) == 0 { - return errors.ErrInvalid("credential setting", s) - } - } - if len(attrs) != 0 { - o.Context.CredentialsContext().SetCredentialsForConsumer(id, credentials.NewCredentials(attrs)) - } else if len(id) != 0 { - return errors.Newf("empty credential attribute set for %s", id.String()) - } - - set, err := common2.ParseLabels(o.Context.FileSystem(), o.Settings, "attribute setting") + err = registration.RegisterExtensions(o.Context) if err != nil { - return errors.Wrapf(err, "invalid attribute setting") - } - if len(set) > 0 { - ctx := o.Context.ConfigContext() - spec := datacfg.New() - for _, s := range set { - attr := s.Name - eff := datacontext.DefaultAttributeScheme.Shortcuts()[attr] - if eff != "" { - attr = eff - } - err = spec.AddRawAttribute(attr, s.Value) - if err != nil { - return errors.Wrapf(err, "attribute %s", s.Name) - } - } - _ = ctx.ApplyConfig(spec, "cli") + return err } - return registration.RegisterExtensions(o.Context.OCMContext()) + return o.Context.ConfigContext().Validate() } func prepare(s string) string { diff --git a/cmds/ocm/app/app_test.go b/cmds/ocm/app/app_test.go index 16ada9f5f1..088c1b5cc6 100644 --- a/cmds/ocm/app/app_test.go +++ b/cmds/ocm/app/app_test.go @@ -64,9 +64,10 @@ var _ = Describe("Test Environment", func() { var m map[string]interface{} Expect(json.Unmarshal(buf.Bytes(), &m)).To(Succeed()) }) + It("do logging", func() { buf := bytes.NewBuffer(nil) - Expect(env.CatchOutput(buf).ExecuteModified(addTestCommands, "logtest")).To(Succeed()) + Expect(env.CatchOutput(buf).ExecuteModified(addTestCommands, "-X", "plugindir=xxx", "logtest")).To(Succeed()) Expect(log.String()).To(StringEqualTrimmedWithContext(` V[2] warn realm ocm realm test ERROR error realm ocm realm test @@ -135,9 +136,15 @@ ERROR ctxerror realm ocm realm test Expect(err).To(Succeed()) fmt.Printf("%s\n", string(data)) - // {"level":"error","msg":"error","realm":"test","time":"2024-03-27 09:54:19"} - // {"level":"error","msg":"ctxerror","realm":"test","time":"2024-03-27 09:54:19"} - Expect(len(string(data))).To(Equal(312)) + // 2024-06-16T13:59:34+02:00 warning [test] warn + // 2024-06-16T13:59:34+02:00 error [test] error + // 2024-06-16T13:59:34+02:00 warning [test] ctxwarn + // 2024-06-16T13:59:34+02:00 error [test] ctxerror + Expect(string(data)).To(MatchRegexp(`.* warning \[test\] warn +.* error \[test\] error +.* warning \[test\] ctxwarn +.* error \[test\] ctxerror +`)) }) It("sets attr from file", func() { diff --git a/cmds/ocm/app/prepare.go b/cmds/ocm/app/prepare.go index a3b6f040ab..4bbbc1b36c 100644 --- a/cmds/ocm/app/prepare.go +++ b/cmds/ocm/app/prepare.go @@ -60,7 +60,12 @@ func Prepare(ctx clictx.Context, args []string) (*CLIOptions, []string, error) { if help { args = append([]string{"--help"}, args...) } - return opts, args, opts.Complete() + + err = opts.Complete() + if err != nil { + return nil, nil, err + } + return opts, args, nil } func hasNoOptDefVal(name string, fs *pflag.FlagSet) bool { diff --git a/cmds/ocm/clippi/config/evaluated.go b/cmds/ocm/clippi/config/evaluated.go new file mode 100644 index 0000000000..ba4dabeda3 --- /dev/null +++ b/cmds/ocm/clippi/config/evaluated.go @@ -0,0 +1,13 @@ +package config + +import ( + "github.com/open-component-model/ocm/cmds/ocm/commands/common/options/keyoption" + "github.com/open-component-model/ocm/pkg/cobrautils/logopts" + "github.com/open-component-model/ocm/pkg/contexts/config" +) + +type EvaluatedOptions struct { + LogOpts *logopts.EvaluatedOptions + Keys *keyoption.EvaluatedOptions + ConfigForward config.Config +} diff --git a/cmds/ocm/clippi/config/type.go b/cmds/ocm/clippi/config/type.go new file mode 100644 index 0000000000..bfdeac596d --- /dev/null +++ b/cmds/ocm/clippi/config/type.go @@ -0,0 +1,232 @@ +package config + +import ( + "strings" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/pflag" + + "github.com/open-component-model/ocm/cmds/ocm/commands/common/options/keyoption" + common2 "github.com/open-component-model/ocm/pkg/clisupport" + "github.com/open-component-model/ocm/pkg/cobrautils/logopts" + logdata "github.com/open-component-model/ocm/pkg/cobrautils/logopts/logging" + "github.com/open-component-model/ocm/pkg/common" + "github.com/open-component-model/ocm/pkg/contexts/config" + config2 "github.com/open-component-model/ocm/pkg/contexts/config/config" + cfgcpi "github.com/open-component-model/ocm/pkg/contexts/config/cpi" + "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/datacontext/attrs/vfsattr" + datacfg "github.com/open-component-model/ocm/pkg/contexts/datacontext/config/attrs" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/signingattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/utils" + "github.com/open-component-model/ocm/pkg/logging" + "github.com/open-component-model/ocm/pkg/runtime" + "github.com/open-component-model/ocm/pkg/signing" +) + +const ( + ConfigType = "cli.ocm" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX + ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1" +) + +func init() { + cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage)) + cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage)) +} + +// Config describes a memory based config interface. +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + + ConfigSets []string `json:"configSets,omitempty"` + Credentials []string `json:"credentials,omitempty"` + Settings []string `json:"settings,omitempty"` + Verbose bool `json:"verbose,omitempty"` + Signing keyoption.ConfigFragment `json:"signing,omitempty"` + Logging logopts.ConfigFragment `json:"logging,omitempty"` + + // ConfigFiles describes the cli argument for additional config files. + // This is not persisted since it is resolved by the first evaluation. + ConfigFiles []string `json:"-"` +} + +// New creates a new memory ConfigSpec. +func New() *Config { + return &Config{ + ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType), + } +} + +func (c *Config) GetType() string { + return ConfigType +} + +func (c *Config) AddFlags(fs *pflag.FlagSet) { + fs.StringArrayVarP(&c.ConfigFiles, "config", "", nil, "configuration file") + fs.StringSliceVarP(&c.ConfigSets, "config-set", "", nil, "apply configuration set") + fs.StringArrayVarP(&c.Credentials, "cred", "C", nil, "credential setting") + fs.StringArrayVarP(&c.Settings, "attribute", "X", nil, "attribute setting") + fs.BoolVarP(&c.Verbose, "verbose", "v", false, "deprecated: enable logrus verbose logging") + + c.Logging.AddFlags(fs) + c.Signing.AddFlags(fs) +} + +func (c *Config) Evaluate(ctx ocm.Context, main bool) (*EvaluatedOptions, error) { + c.Type = ConfigTypeV1 + cfg, err := config2.NewAggregator(true) + if err != nil { + return nil, err + } + + logopts, err := c.Logging.Evaluate(ctx, logging.Context(), main) + if err != nil { + return nil, err + } + opts := &EvaluatedOptions{LogOpts: logopts} + + opts.Keys, err = c.Signing.Evaluate(ctx, nil) + if err != nil { + return opts, err + } + + if len(c.ConfigFiles) == 0 { + _, eff, err := utils.Configure2(ctx, "", vfsattr.Get(ctx)) + if eff != nil { + err = cfg.AddConfig(eff) + } + if err != nil { + return opts, err + } + } + for _, config := range c.ConfigFiles { + _, eff, err := utils.Configure2(ctx, config, vfsattr.Get(ctx)) + if eff != nil { + err = cfg.AddConfig(eff) + } + if err != nil { + return opts, err + } + } + + keyopts, err := c.Signing.Evaluate(ctx, nil) + if err != nil { + return opts, err + } + + if keyopts.Keys.HasKeys() { + def := signingattr.Get(ctx) + err = signingattr.Set(ctx, signing.NewRegistry(def.HandlerRegistry(), signing.NewKeyRegistry(keyopts.Keys, def.KeyRegistry()))) + if err != nil { + return opts, err + } + } + + for _, n := range c.ConfigSets { + err := ctx.ConfigContext().ApplyConfigSet(n) + if err != nil { + return opts, err + } + } + + id := credentials.ConsumerIdentity{} + attrs := common.Properties{} + for _, s := range c.Credentials { + i := strings.Index(s, "=") + if i < 0 { + return opts, errors.ErrInvalid("credential setting", s) + } + name := s[:i] + value := s[i+1:] + if strings.HasPrefix(name, ":") { + if len(attrs) != 0 { + ctx.CredentialsContext().SetCredentialsForConsumer(id, credentials.NewCredentials(attrs)) + id = credentials.ConsumerIdentity{} + attrs = common.Properties{} + } + name = name[1:] + id[name] = value + } else { + attrs[name] = value + } + if len(name) == 0 { + return opts, errors.ErrInvalid("credential setting", s) + } + } + if len(attrs) != 0 { + ctx.CredentialsContext().SetCredentialsForConsumer(id, credentials.NewCredentials(attrs)) + } else if len(id) != 0 { + return opts, errors.Newf("empty credential attribute set for %s", id.String()) + } + + set, err := common2.ParseLabels(vfsattr.Get(ctx), c.Settings, "attribute setting") + if err != nil { + return opts, errors.Wrapf(err, "invalid attribute setting") + } + if len(set) > 0 { + cfgctx := ctx.ConfigContext() + spec := datacfg.New() + for _, s := range set { + attr := s.Name + eff := datacontext.DefaultAttributeScheme.Shortcuts()[attr] + if eff != "" { + attr = eff + } + err = spec.AddRawAttribute(attr, s.Value) + if err != nil { + return opts, errors.Wrapf(err, "attribute %s", s.Name) + } + } + _ = cfgctx.ApplyConfig(spec, "cli") + } + cfg.AddConfig(c) + opts.ConfigForward = cfg.Get() + return opts, nil +} + +func (c *Config) ApplyTo(cctx config.Context, target interface{}) error { + // first: check for logging config for subsequent command calls + if lc, ok := target.(*logdata.LoggingConfiguration); ok { + cfg, err := c.Logging.GetLogConfig(vfsattr.Get(cctx)) + if err != nil { + return err + } + lc.LogConfig = *cfg + lc.Json = c.Logging.Json + return nil + } + + // second: main target is an ocm context + ctx, ok := target.(cpi.Context) + if !ok { + return config.ErrNoContext(ConfigType) + } + + opts, err := c.Evaluate(ctx, false) + if err != nil { + return err + } + if opts.Keys.Keys.HasKeys() { + def := signingattr.Get(ctx) + err = signingattr.Set(ctx, signing.NewRegistry(def.HandlerRegistry(), signing.NewKeyRegistry(opts.Keys.Keys, def.KeyRegistry()))) + if err != nil { + return err + } + } + return nil +} + +const usage = ` +The config type ` + ConfigType + ` is used to handle the +main configuration flags of the OCM command line tool. + +
+    type: ` + ConfigType + `
+    aliases:
+       <name>: <OCI registry specification>
+       ...
+
+` diff --git a/cmds/ocm/commands/common/options/keyoption/config.go b/cmds/ocm/commands/common/options/keyoption/config.go new file mode 100644 index 0000000000..72ea17d46d --- /dev/null +++ b/cmds/ocm/commands/common/options/keyoption/config.go @@ -0,0 +1,119 @@ +package keyoption + +import ( + "crypto/x509" + "fmt" + "reflect" + "strings" + + "github.com/mandelsoft/goutils/errors" + "github.com/spf13/pflag" + + "github.com/open-component-model/ocm/pkg/contexts/datacontext" + "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/vfsattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/signing" + "github.com/open-component-model/ocm/pkg/signing/signutils" + "github.com/open-component-model/ocm/pkg/utils" +) + +type ConfigFragment struct { + DefaultName string `json:"defaultName,omitempty"` + PublicKeys []string `json:"publicKeys,omitempty"` + PrivateKeys []string `json:"privateKeys,omitempty"` + Issuers []string `json:"issuers,omitempty"` + RootCAs []string `json:"rootCAs,omitempty"` +} + +func (c *ConfigFragment) AddFlags(fs *pflag.FlagSet) { + fs.StringArrayVarP(&c.PublicKeys, "public-key", "k", nil, "public key setting") + fs.StringArrayVarP(&c.PrivateKeys, "private-key", "K", nil, "private key setting") + fs.StringArrayVarP(&c.Issuers, "issuer", "I", nil, "issuer name or distinguished name (DN) (optionally for dedicated signature) ([:=]") + fs.StringArrayVarP(&c.RootCAs, "ca-cert", "", nil, "additional root certificate authorities (for signing certificates)") +} + +func (c *ConfigFragment) Evaluate(ctx ocm.Context, keys signing.KeyRegistry) (*EvaluatedOptions, error) { + var opts EvaluatedOptions + + if keys == nil { + keys = signing.NewKeyRegistry() + } + opts.Keys = keys + + err := c.HandleKeys(ctx, "public key", c.PublicKeys, keys.RegisterPublicKey) + if err != nil { + return nil, err + } + err = c.HandleKeys(ctx, "private key", c.PrivateKeys, keys.RegisterPrivateKey) + if err != nil { + return nil, err + } + for _, i := range c.Issuers { + name := c.DefaultName + is := i + sep := strings.Index(i, ":=") + if sep >= 0 { + name = i[:sep] + is = i[sep+1:] + } + old := keys.GetIssuer(name) + dn, err := signutils.ParseDN(is) + if err != nil { + return nil, errors.Wrapf(err, "issuer %q", i) + } + if old != nil && !reflect.DeepEqual(old, dn) { + return nil, fmt.Errorf("issuer already set (%s)", i) + } + + keys.RegisterIssuer(name, dn) + } + + if len(c.RootCAs) > 0 { + var list []*x509.Certificate + for _, r := range c.RootCAs { + data, err := utils.ReadFile(r, vfsattr.Get(ctx)) + if err != nil { + return nil, errors.Wrapf(err, "root CA") + } + certs, err := signutils.GetCertificateChain(data, false) + if err != nil { + return nil, errors.Wrapf(err, "root CA") + } + list = append(list, certs...) + } + opts.RootCerts = list + } + return &opts, nil +} + +func (c *ConfigFragment) HandleKeys(ctx datacontext.Context, desc string, keys []string, add func(string, interface{})) error { + name := c.DefaultName + fs := vfsattr.Get(ctx) + for _, k := range keys { + file := k + sep := strings.Index(k, "=") + if sep > 0 { + name = k[:sep] + file = k[sep+1:] + } + if len(file) == 0 { + return errors.Newf("%s: empty file name", desc) + } + var data []byte + var err error + switch file[0] { + case '=', '!', '@': + data, err = utils.ResolveData(file, fs) + default: + data, err = utils.ReadFile(file, fs) + } + if err != nil { + return errors.Wrapf(err, "cannot read %s file %q", desc, file) + } + if name == "" { + return errors.Newf("%s: key name required", desc) + } + add(name, data) + } + return nil +} diff --git a/cmds/ocm/commands/common/options/keyoption/option.go b/cmds/ocm/commands/common/options/keyoption/option.go index a3102ea969..28f3d67110 100644 --- a/cmds/ocm/commands/common/options/keyoption/option.go +++ b/cmds/ocm/commands/common/options/keyoption/option.go @@ -1,20 +1,13 @@ package keyoption import ( - "crypto/x509" - "fmt" - "reflect" - "strings" - - "github.com/mandelsoft/goutils/errors" "github.com/spf13/pflag" "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/contexts/ocm" ocmsign "github.com/open-component-model/ocm/pkg/contexts/ocm/signing" "github.com/open-component-model/ocm/pkg/signing" "github.com/open-component-model/ocm/pkg/signing/signutils" - "github.com/open-component-model/ocm/pkg/utils" ) func From(o options.OptionSetProvider) *Option { @@ -29,102 +22,24 @@ func New() *Option { return &Option{} } -type Option struct { - DefaultName string - publicKeys []string - privateKeys []string - issuers []string - rootCAs []string - RootCerts signutils.GenericCertificatePool - Keys signing.KeyRegistry +type EvaluatedOptions struct { + RootCerts signutils.GenericCertificatePool + Keys signing.KeyRegistry } -func (o *Option) AddFlags(fs *pflag.FlagSet) { - fs.StringArrayVarP(&o.publicKeys, "public-key", "k", nil, "public key setting") - fs.StringArrayVarP(&o.privateKeys, "private-key", "K", nil, "private key setting") - fs.StringArrayVarP(&o.issuers, "issuer", "I", nil, "issuer name or distinguished name (DN) (optionally for dedicated signature) ([:=]") - fs.StringArrayVarP(&o.rootCAs, "ca-cert", "", nil, "additional root certificate authorities (for signing certificates)") +type Option struct { + ConfigFragment + *EvaluatedOptions } -func (o *Option) Configure(ctx clictx.Context) error { - if o.Keys == nil { - o.Keys = signing.NewKeyRegistry() - } - err := o.HandleKeys(ctx, "public key", o.publicKeys, o.Keys.RegisterPublicKey) - if err != nil { - return err - } - err = o.HandleKeys(ctx, "private key", o.privateKeys, o.Keys.RegisterPrivateKey) - if err != nil { - return err - } - for _, i := range o.issuers { - name := o.DefaultName - is := i - sep := strings.Index(i, ":=") - if sep >= 0 { - name = i[:sep] - is = i[sep+1:] - } - old := o.Keys.GetIssuer(name) - dn, err := signutils.ParseDN(is) - if err != nil { - return errors.Wrapf(err, "issuer %q", i) - } - if old != nil && !reflect.DeepEqual(old, dn) { - return fmt.Errorf("issuer already set (%s)", i) - } - - o.Keys.RegisterIssuer(name, dn) - } - - if len(o.rootCAs) > 0 { - var list []*x509.Certificate - for _, r := range o.rootCAs { - data, err := utils.ReadFile(r, ctx.FileSystem()) - if err != nil { - return errors.Wrapf(err, "root CA") - } - certs, err := signutils.GetCertificateChain(data, false) - if err != nil { - return errors.Wrapf(err, "root CA") - } - list = append(list, certs...) - } - o.RootCerts = list - } - return nil +func (o *Option) AddFlags(fs *pflag.FlagSet) { + o.ConfigFragment.AddFlags(fs) } -func (o *Option) HandleKeys(ctx clictx.Context, desc string, keys []string, add func(string, interface{})) error { - name := o.DefaultName - for _, k := range keys { - file := k - sep := strings.Index(k, "=") - if sep > 0 { - name = k[:sep] - file = k[sep+1:] - } - if len(file) == 0 { - return errors.Newf("%s: empty file name", desc) - } - var data []byte - var err error - switch file[0] { - case '=', '!', '@': - data, err = utils.ResolveData(file, ctx.FileSystem()) - default: - data, err = utils.ReadFile(file, ctx.FileSystem()) - } - if err != nil { - return errors.Wrapf(err, "cannot read %s file %q", desc, file) - } - if name == "" { - return errors.Newf("%s: key name required", desc) - } - add(name, data) - } - return nil +func (o *Option) Configure(ctx ocm.Context) error { + var err error + o.EvaluatedOptions, err = o.ConfigFragment.Evaluate(ctx, nil) + return err } func Usage() string { diff --git a/cmds/ocm/commands/misccmds/config/cmd.go b/cmds/ocm/commands/misccmds/config/cmd.go new file mode 100644 index 0000000000..d50ff7b3e5 --- /dev/null +++ b/cmds/ocm/commands/misccmds/config/cmd.go @@ -0,0 +1,21 @@ +package credentials + +import ( + "github.com/spf13/cobra" + + config "github.com/open-component-model/ocm/cmds/ocm/commands/misccmds/config/get" + "github.com/open-component-model/ocm/cmds/ocm/commands/misccmds/names" + "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + "github.com/open-component-model/ocm/pkg/contexts/clictx" +) + +var Names = names.Config + +// NewCommand creates a new command. +func NewCommand(ctx clictx.Context) *cobra.Command { + cmd := utils.MassageCommand(&cobra.Command{ + Short: "Commands acting on CLI config", + }, Names...) + cmd.AddCommand(config.NewCommand(ctx, config.Verb)) + return cmd +} diff --git a/cmds/ocm/commands/misccmds/config/get/cmd.go b/cmds/ocm/commands/misccmds/config/get/cmd.go new file mode 100644 index 0000000000..94af2d854d --- /dev/null +++ b/cmds/ocm/commands/misccmds/config/get/cmd.go @@ -0,0 +1,77 @@ +package get + +import ( + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/open-component-model/ocm/cmds/ocm/commands/common/options/destoption" + "github.com/open-component-model/ocm/cmds/ocm/commands/misccmds/names" + "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" + "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/clicfgattr" + "github.com/open-component-model/ocm/pkg/out" +) + +var ( + Names = names.Config + Verb = verbs.Get +) + +type Command struct { + utils.BaseCommand +} + +var _ utils.OCMCommand = (*Command)(nil) + +// NewCommand creates a new artifact command. +func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { + return utils.SetupCommand(&Command{BaseCommand: utils.NewBaseCommand(ctx, output.OutputOptions(outputs), destoption.New())}, utils.Names(Names, names...)...) +} + +func (o *Command) ForName(name string) *cobra.Command { + return &cobra.Command{ + Use: "", + Short: "Get evaluated config for actual command call", + Long: ` +Evaluate the command line arguments and all explicitly +or implicitly used configuration files and provide +a single configuration object. +`, + } +} + +func (o *Command) AddFlags(set *pflag.FlagSet) { + o.BaseCommand.AddFlags(set) +} + +func (o *Command) Run() error { + cfg := clicfgattr.Get(o.Context) + if cfg == nil { + out.Outf(o.Context, "no configuration found") + return nil + } + opts := output.From(o) + + opts.Output.Add(output.AsManifest(cfg)) + + dest := destoption.From(o) + if dest.Destination != "" { + file, err := dest.PathFilesystem.OpenFile(dest.Destination, vfs.O_CREATE|vfs.O_TRUNC|vfs.O_WRONLY, 0o600) + if err != nil { + return errors.Wrapf(err, "cannot create output file %q", dest.Destination) + } + opts.Output.(output.Destination).SetDestination(file) + defer file.Close() + } + err := opts.Output.Out() + if err == nil && dest.Destination != "" { + out.Outf(o.Context, "config written to %q\n", dest.Destination) + } + return err +} + +var outputs = output.NewOutputs(output.DefaultYAMLOutput).AddManifestOutputs() diff --git a/cmds/ocm/commands/misccmds/config/get/cmd_test.go b/cmds/ocm/commands/misccmds/config/get/cmd_test.go new file mode 100644 index 0000000000..68da28556e --- /dev/null +++ b/cmds/ocm/commands/misccmds/config/get/cmd_test.go @@ -0,0 +1,47 @@ +package get_test + +import ( + "bytes" + "encoding/json" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/cmds/ocm/testhelper" + + "github.com/mandelsoft/vfs/pkg/vfs" +) + +var _ = Describe("Get config", func() { + var env *TestEnv + + BeforeEach(func() { + env = NewTestEnv() + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("provides json output", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("get", "config", "-o", "json")) + + var r map[string]interface{} + MustBeSuccessful(json.Unmarshal(buf.Bytes(), &r)) + }) + + It("writes json output", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("get", "config", "-o", "json", "-O", "config")) + + Expect(buf.String()).To(Equal("config written to \"config\"\n")) + Expect(vfs.Exists(env.FileSystem(), "config")).To(BeTrue()) + + data := Must(vfs.ReadFile(env.FileSystem(), "config")) + var r map[string]interface{} + MustBeSuccessful(json.Unmarshal(data, &r)) + }) +}) diff --git a/cmds/ocm/commands/misccmds/config/get/suite_test.go b/cmds/ocm/commands/misccmds/config/get/suite_test.go new file mode 100644 index 0000000000..db4d59bb59 --- /dev/null +++ b/cmds/ocm/commands/misccmds/config/get/suite_test.go @@ -0,0 +1,13 @@ +package get_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OCM get config command") +} diff --git a/cmds/ocm/commands/misccmds/names/names.go b/cmds/ocm/commands/misccmds/names/names.go index 727abbee0d..62ae6cf513 100644 --- a/cmds/ocm/commands/misccmds/names/names.go +++ b/cmds/ocm/commands/misccmds/names/names.go @@ -4,4 +4,5 @@ var ( Hash = []string{"hash"} RSAKeyPair = []string{"rsakeypair", "rsa"} Credentials = []string{"credentials", "creds", "cred"} + Config = []string{"config", "cfg"} ) diff --git a/cmds/ocm/commands/ocmcmds/common/handlers/comphdlr/typehandler.go b/cmds/ocm/commands/ocmcmds/common/handlers/comphdlr/typehandler.go index 8e11a6ed8a..82ca5e7e8e 100644 --- a/cmds/ocm/commands/ocmcmds/common/handlers/comphdlr/typehandler.go +++ b/cmds/ocm/commands/ocmcmds/common/handlers/comphdlr/typehandler.go @@ -223,6 +223,9 @@ func (h *TypeHandler) get(repo ocm.Repository, elemspec utils.ElemSpec) ([]outpu }) } else { if component == nil { + if repo == nil { + return nil, errors.Wrapf(err, "%s: invalid component version reference", name) + } return h.all(repo) } else { versions, err := component.ListVersions() diff --git a/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go b/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go index 1bbe9d0597..131f1a9009 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go +++ b/cmds/ocm/commands/ocmcmds/common/options/signoption/option.go @@ -111,7 +111,7 @@ func (o *Option) Configure(ctx clictx.Context) error { o.Recursively = !o.local } - err := o.Option.Configure(ctx) + err := o.Option.Configure(ctx.OCMContext()) if err != nil { return err } diff --git a/cmds/ocm/commands/ocmcmds/plugins/describe/cmd_test.go b/cmds/ocm/commands/ocmcmds/plugins/describe/cmd_test.go index 87cd4be62b..28e6c866e5 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/describe/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/plugins/describe/cmd_test.go @@ -9,37 +9,33 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/open-component-model/ocm/cmds/ocm/testhelper" - - "github.com/mandelsoft/filepath/pkg/filepath" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" ) const PLUGINS = "/testdata" var _ = Describe("Test Environment", func() { var env *TestEnv - var path string + var plugins TempPluginDir BeforeEach(func() { - env = NewTestEnv(TestData()) - - // use os filesystem here - p, err := filepath.Abs("testdata") - Expect(err).To(Succeed()) - path = p + env = NewTestEnv() + plugins = Must(ConfigureTestPlugins(env, "testdata")) }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) It("get plugins", func() { buf := bytes.NewBuffer(nil) - Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+path, "describe", "plugins")).To(Succeed()) + Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+plugins.Path(), "describe", "plugins")).To(Succeed()) Expect(buf.String()).To(StringEqualTrimmedWithContext( ` Plugin Name: action Plugin Version: v1 -Path: ` + path + `/action +Path: ` + plugins.Path() + `/action Status: valid Source: manually installed Capabilities: Actions @@ -64,7 +60,7 @@ Actions: ---------------------- Plugin Name: test Plugin Version: v1 -Path: ` + path + `/test +Path: ` + plugins.Path() + `/test Status: valid Source: manually installed Capabilities: Access Methods diff --git a/cmds/ocm/commands/ocmcmds/plugins/describe/describe.go b/cmds/ocm/commands/ocmcmds/plugins/describe/describe.go index 67da8d3f48..5aacc222cc 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/describe/describe.go +++ b/cmds/ocm/commands/ocmcmds/plugins/describe/describe.go @@ -19,8 +19,8 @@ func DescribePlugin(p plugin.Plugin, out common.Printer) { } out.Printf("Status: %s\n", "valid") d := p.GetDescriptor() - src := p.GetSource() - if src != nil { + src := p.GetInstallationInfo() + if src != nil && src.HasSourceInfo() { out.Printf("Source:\n") out.Printf(" Component: %s\n", src.Component) out.Printf(" Version: %s\n", src.Version) diff --git a/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go b/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go index b3bcf92cd8..212bdc0d31 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go +++ b/cmds/ocm/commands/ocmcmds/plugins/get/cmd.go @@ -93,35 +93,9 @@ func getWide(opts *output.Options) output.Output { func mapGetRegularOutput(e interface{}) interface{} { p := handler.Elem(e) - loc := "local" - src := p.GetSource() - if src != nil { - loc = src.Component + ":" + src.Version - } - - var features []string - if len(p.GetDescriptor().AccessMethods) > 0 { - features = append(features, "accessmethods") - } - if len(p.GetDescriptor().Uploaders) > 0 { - features = append(features, "uploaders") - } - if len(p.GetDescriptor().Downloaders) > 0 { - features = append(features, "downloaders") - } - if len(p.GetDescriptor().Actions) > 0 { - features = append(features, "actions") - } - if len(p.GetDescriptor().ValueSets) > 0 { - features = append(features, "valuesets") - } - if len(p.GetDescriptor().ValueMergeHandlers) > 0 { - features = append(features, "mergehandlers") - } - if len(p.GetDescriptor().LabelMergeSpecifications) > 0 { - features = append(features, "mergespecs") - } + loc := p.GetInstallationInfo().GetInstallationSourceDescription() + features := p.GetDescriptor().Capabilities() return []string{p.Name(), p.Version(), loc, p.Message(), strings.Join(features, ",")} } diff --git a/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go b/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go index 2ae60616f5..4ffb886a32 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/plugins/get/cmd_test.go @@ -9,23 +9,18 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/open-component-model/ocm/cmds/ocm/testhelper" - - "github.com/mandelsoft/filepath/pkg/filepath" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" ) const PLUGINS = "/testdata" var _ = Describe("Test Environment", func() { var env *TestEnv - var path string + var plugins TempPluginDir BeforeEach(func() { - env = NewTestEnv(TestData()) - - // use os filesystem here - p, err := filepath.Abs("testdata") - Expect(err).To(Succeed()) - path = p + env = NewTestEnv() + plugins = Must(ConfigureTestPlugins(env, "testdata")) }) AfterEach(func() { @@ -34,16 +29,16 @@ var _ = Describe("Test Environment", func() { It("get plugins", func() { buf := bytes.NewBuffer(nil) - Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+path, "get", "plugins")).To(Succeed()) + Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+plugins.Path(), "get", "plugins")).To(Succeed()) Expect(buf.String()).To(StringEqualTrimmedWithContext( ` PLUGIN VERSION SOURCE DESCRIPTION CAPABILITIES -test v1 local a test plugin without function accessmethods +test v1 local a test plugin without function Access Methods `)) }) It("get plugins with additional info", func() { buf := bytes.NewBuffer(nil) - Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+path, "get", "plugins", "-o", "wide")).To(Succeed()) + Expect(env.CatchOutput(buf).Execute("-X", "plugindir="+plugins.Path(), "get", "plugins", "-o", "wide")).To(Succeed()) Expect(buf.String()).To(StringEqualTrimmedWithContext( ` PLUGIN VERSION SOURCE DESCRIPTION ACCESSMETHODS UPLOADERS DOWNLOADERS ACTIONS diff --git a/cmds/ocm/commands/ocmcmds/plugins/tests/accessmethods/cmd_test.go b/cmds/ocm/commands/ocmcmds/plugins/tests/accessmethods/cmd_test.go index 25339d307d..d6f3c973b1 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/tests/accessmethods/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/plugins/tests/accessmethods/cmd_test.go @@ -7,10 +7,9 @@ import ( . "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/ocm/plugin/testutils" "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/compdesc" metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" @@ -28,13 +27,13 @@ var _ = Describe("Add with new access method", func() { var env *TestEnv var ctx ocm.Context var registry plugins.Set + var plugins TempPluginDir BeforeEach(func() { env = NewTestEnv(TestData()) ctx = env.OCMContext() + plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata")) - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") Expect(p).NotTo(BeNil()) @@ -43,6 +42,7 @@ var _ = Describe("Add with new access method", func() { }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/cmds/ocm/commands/ocmcmds/plugins/tests/routingslips/cmd_test.go b/cmds/ocm/commands/ocmcmds/plugins/tests/routingslips/cmd_test.go index 8f8b72c1f3..4d8ac35746 100644 --- a/cmds/ocm/commands/ocmcmds/plugins/tests/routingslips/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/plugins/tests/routingslips/cmd_test.go @@ -7,11 +7,11 @@ import ( . "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/ocm/plugin/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/ocm/attrs/plugincacheattr" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugindirattr" "github.com/open-component-model/ocm/pkg/contexts/ocm/labels/routingslip" "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" @@ -26,6 +26,7 @@ const ( var _ = Describe("Test Environment", func() { var env *TestEnv + var plugins TempPluginDir BeforeEach(func() { env = NewTestEnv() @@ -39,7 +40,8 @@ var _ = Describe("Test Environment", func() { env.RSAKeyPair(PROVIDER) ctx := env.OCMContext() - plugindirattr.Set(ctx, "testdata") + plugins = Must(ConfigureTestPlugins(env, "testdata")) + registry := plugincacheattr.Get(ctx) Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") @@ -47,6 +49,7 @@ var _ = Describe("Test Environment", func() { }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/cmds/ocm/commands/plugin/cmd.go b/cmds/ocm/commands/plugin/cmd.go new file mode 100644 index 0000000000..c226f221c7 --- /dev/null +++ b/cmds/ocm/commands/plugin/cmd.go @@ -0,0 +1,63 @@ +package plugin + +import ( + "strings" + + "github.com/mandelsoft/goutils/sliceutils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "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" +) + +type Command struct { + utils.BaseCommand + plugin plugin.Plugin + pcmd *plugin.CommandDescriptor + name string + + args []string +} + +var _ utils.OCMCommand = (*Command)(nil) + +// NewCommand creates a new plugin based command. +func NewCommand(ctx clictx.Context, plugin plugin.Plugin, name string, names ...string) *cobra.Command { + me := &Command{BaseCommand: utils.NewBaseCommand(ctx)} + me.plugin = plugin + me.name = name + me.pcmd = plugin.GetDescriptor().Commands.Get(name) + + cmd := utils.SetupCommand(me, utils.Names([]string{me.pcmd.Name}, names...)...) + cmd.DisableFlagParsing = true + cmd.SetHelpFunc(me.help) + return cmd +} + +func (o *Command) ForName(name string) *cobra.Command { + pcmd := o.plugin.GetDescriptor().Commands.Get(o.name) + return &cobra.Command{ + Use: pcmd.Usage[strings.Index(pcmd.Usage, " ")+1:], + Short: pcmd.Short, + Long: pcmd.GetDescription(), + Example: pcmd.Example, + } +} + +func (o *Command) AddFlags(set *pflag.FlagSet) { +} + +func (o *Command) Complete(args []string) error { + o.args = args + return nil +} + +func (o *Command) Run() error { + return o.plugin.Command(o.name, o.StdIn(), o.StdOut(), o.args) +} + +func (o *Command) help(cmd *cobra.Command, args []string) { + o.plugin.Command(o.name, o.StdIn(), o.StdOut(), sliceutils.CopyAppend(args, "--help")) +} diff --git a/cmds/ocm/commands/verbs/get/cmd.go b/cmds/ocm/commands/verbs/get/cmd.go index 61d22474b1..ec96f4eb7e 100644 --- a/cmds/ocm/commands/verbs/get/cmd.go +++ b/cmds/ocm/commands/verbs/get/cmd.go @@ -3,6 +3,7 @@ package get import ( "github.com/spf13/cobra" + config "github.com/open-component-model/ocm/cmds/ocm/commands/misccmds/config/get" credentials "github.com/open-component-model/ocm/cmds/ocm/commands/misccmds/credentials/get" artifacts "github.com/open-component-model/ocm/cmds/ocm/commands/ocicmds/artifacts/get" components "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/components/get" @@ -29,5 +30,6 @@ func NewCommand(ctx clictx.Context) *cobra.Command { cmd.AddCommand(credentials.NewCommand(ctx)) cmd.AddCommand(plugins.NewCommand(ctx)) cmd.AddCommand(routingslips.NewCommand(ctx)) + cmd.AddCommand(config.NewCommand(ctx)) return cmd } diff --git a/cmds/ocm/commands/verbs/verb.go b/cmds/ocm/commands/verbs/verb.go new file mode 100644 index 0000000000..50f5310c7f --- /dev/null +++ b/cmds/ocm/commands/verbs/verb.go @@ -0,0 +1,16 @@ +package verbs + +import ( + "github.com/spf13/cobra" + + "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, name string, short string) *cobra.Command { + cmd := utils.MassageCommand(&cobra.Command{ + Short: short, + }, name) + return cmd +} diff --git a/cmds/ocm/pkg/output/elementoutput.go b/cmds/ocm/pkg/output/elementoutput.go index 0c3adb3338..9f1a25de0a 100644 --- a/cmds/ocm/pkg/output/elementoutput.go +++ b/cmds/ocm/pkg/output/elementoutput.go @@ -1,6 +1,9 @@ package output import ( + "fmt" + "io" + . "github.com/open-component-model/ocm/cmds/ocm/pkg/processing" . "github.com/open-component-model/ocm/pkg/out" @@ -9,11 +12,46 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/pkg/data" ) -type ElementOutput struct { - source ProcessingSource - Elems data.Iterable +type DestinationOutput struct { Context Context - Status error + out io.Writer +} + +var _ Destination = (*DestinationOutput)(nil) + +func (this *DestinationOutput) SetDestination(d io.Writer) { + this.out = d +} + +func (this *DestinationOutput) Printf(msg string, args ...interface{}) { + if this.out != nil { + fmt.Fprintf(this.out, msg, args...) + } else { + fmt.Fprintf(this.Context.StdOut(), msg, args...) + } +} + +func (this *DestinationOutput) Print(args ...interface{}) { + if this.out != nil { + fmt.Fprint(this.out, args...) + } else { + fmt.Fprint(this.Context.StdOut(), args...) + } +} + +func (this *DestinationOutput) Write(data []byte) (int, error) { + if this.out != nil { + return this.out.Write(data) + } else { + return this.Context.StdOut().Write(data) + } +} + +type ElementOutput struct { + DestinationOutput + source ProcessingSource + Elems data.Iterable + Status error } var _ Output = (*ElementOutput)(nil) diff --git a/cmds/ocm/pkg/output/output.go b/cmds/ocm/pkg/output/output.go index 7ee771334d..831e72fd93 100644 --- a/cmds/ocm/pkg/output/output.go +++ b/cmds/ocm/pkg/output/output.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" . "github.com/open-component-model/ocm/pkg/out" @@ -31,6 +32,12 @@ type Output interface { Out() error } +// Destination is an optional interface for outputs to +// set the payload output stream to use. +type Destination interface { + SetDestination(io.Writer) +} + //////////////////////////////////////////////////////////////////////////////// type NopOutput struct{} @@ -55,18 +62,32 @@ type Manifest interface { AsManifest() interface{} } +type manifest struct { + data interface{} +} + +func (m *manifest) AsManifest() interface{} { + return m.data +} + +func AsManifest(i interface{}) Manifest { + return &manifest{i} +} + type ManifestOutput struct { - opts *Options - data []Object - Status error - Context Context + DestinationOutput + opts *Options + data []Object + Status error } func NewManifestOutput(opts *Options) ManifestOutput { return ManifestOutput{ - opts: opts, - Context: opts.Context, - data: []Object{}, + DestinationOutput: DestinationOutput{ + Context: opts.Context, + }, + opts: opts, + data: []Object{}, } } @@ -92,12 +113,12 @@ type YAMLOutput struct { func (this *YAMLOutput) Out() error { for _, m := range this.data { - Outf(this.Context, "---\n") + this.Print("---\n") d, err := yaml.Marshal(m.(Manifest).AsManifest()) if err != nil { return err } - this.Context.StdOut().Write(d) + this.Write(d) } return this.ManifestOutput.Out() } @@ -163,7 +184,7 @@ func (this *JSONOutput) Out() error { buf.WriteByte('\n') d = buf.Bytes() } - this.Context.StdOut().Write(d) + this.Write(d) return this.ManifestOutput.Out() } @@ -280,6 +301,10 @@ func (this Outputs) AddChainedManifestOutputs(chain ChainFunction) Outputs { return this } +func DefaultYAMLOutput(opts *Options) Output { + return &YAMLOutput{NewManifestOutput(opts)} +} + var log bool func Print(list []Object, msg string, args ...interface{}) { diff --git a/cmds/subcmdplugin/cmds/cmd_test.go b/cmds/subcmdplugin/cmds/cmd_test.go new file mode 100644 index 0000000000..19d9085de5 --- /dev/null +++ b/cmds/subcmdplugin/cmds/cmd_test.go @@ -0,0 +1,122 @@ +//go:build unix + +package cmds_test + +import ( + "bytes" + + . "github.com/mandelsoft/goutils/testutils" + . "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/ocm/plugin/testutils" + + "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" +) + +var _ = Describe("subcmdplugin", func() { + Context("lib", func() { + var env *TestEnv + var plugins TempPluginDir + + BeforeEach(func() { + env = NewTestEnv(TestData()) + plugins = Must(ConfigureTestPlugins(env, "testdata/plugins")) + + registry := plugincacheattr.Get(env) + // Expect(registration.RegisterExtensions(env)).To(Succeed()) + p := registry.Get("cliplugin") + Expect(p).NotTo(BeNil()) + Expect(p.Error()).To(Equal("")) + }) + + AfterEach(func() { + plugins.Cleanup() + env.Cleanup() + }) + + Context("local help", func() { + It("shows group command help", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("group", "--help")) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +ocm group — A Provided Command Group + +Synopsis: + ocm group + +Available Commands: + demo a demo command + +Flags: + -h, --help help for group + +Description: + A provided command group with a demo command + Use ocm group -h for additional help. +`)) + }) + + It("shows sub command help", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("group", "demo", "--help")) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +ocm group demo — A Demo Command + +Synopsis: + ocm group demo [flags] + +Flags: + -h, --help help for demo + +Description: + a demo command in a provided command group +`)) + }) + }) + + Context("main help", func() { + It("shows group command help", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("help", "group")) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +ocm group — A Provided Command Group + +Synopsis: + ocm group + +Available Commands: + demo a demo command + +Flags: + -h, --help help for group + +Description: + A provided command group with a demo command + Use ocm group -h for additional help. +`)) + }) + + It("shows sub command help", func() { + var buf bytes.Buffer + + MustBeSuccessful(env.CatchOutput(&buf).Execute("help", "group", "demo")) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +ocm group demo — A Demo Command + +Synopsis: + ocm group demo [flags] + +Flags: + -h, --help help for demo + +Description: + a demo command in a provided command group +`)) + }) + }) + }) +}) diff --git a/cmds/subcmdplugin/cmds/demo/cmd.go b/cmds/subcmdplugin/cmds/demo/cmd.go new file mode 100644 index 0000000000..20690f6b0a --- /dev/null +++ b/cmds/subcmdplugin/cmds/demo/cmd.go @@ -0,0 +1,30 @@ +package demo + +import ( + "fmt" + + // bind OCM configuration. + _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/config" + + "github.com/spf13/cobra" +) + +const Name = "demo" + +func New() *cobra.Command { + cmd := &command{} + c := &cobra.Command{ + Use: Name + " ", + Short: "a demo command", + Long: "a demo command in a provided command group", + RunE: cmd.Run, + } + return c +} + +type command struct{} + +func (c *command) Run(cmd *cobra.Command, args []string) error { + fmt.Printf("demo command called with arguments %v\n", args) + return nil +} diff --git a/cmds/subcmdplugin/cmds/group/cmd.go b/cmds/subcmdplugin/cmds/group/cmd.go new file mode 100644 index 0000000000..7f53d1ea08 --- /dev/null +++ b/cmds/subcmdplugin/cmds/group/cmd.go @@ -0,0 +1,20 @@ +package group + +import ( + "github.com/spf13/cobra" + + "github.com/open-component-model/ocm/cmds/subcmdplugin/cmds/demo" +) + +const Name = "group" + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: Name + " ", + Short: "a provided command group", + Long: "A provided command group with a demo command", + } + + cmd.AddCommand(demo.New()) + return cmd +} diff --git a/cmds/subcmdplugin/cmds/suite_test.go b/cmds/subcmdplugin/cmds/suite_test.go new file mode 100644 index 0000000000..5c831e0dde --- /dev/null +++ b/cmds/subcmdplugin/cmds/suite_test.go @@ -0,0 +1,13 @@ +package cmds_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Demo CLI Plugin Command Group Test Suite") +} diff --git a/cmds/subcmdplugin/cmds/testdata/plugins/cliplugin b/cmds/subcmdplugin/cmds/testdata/plugins/cliplugin new file mode 100755 index 0000000000..a29873a10f --- /dev/null +++ b/cmds/subcmdplugin/cmds/testdata/plugins/cliplugin @@ -0,0 +1,2 @@ +#!/bin/bash +go run ../main.go "$@" \ No newline at end of file diff --git a/cmds/subcmdplugin/main.go b/cmds/subcmdplugin/main.go new file mode 100644 index 0000000000..07e0d504aa --- /dev/null +++ b/cmds/subcmdplugin/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "os" + + // enable mandelsoft plugin logging configuration. + _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/logging" + + "github.com/open-component-model/ocm/cmds/subcmdplugin/cmds/group" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/clicmd" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds" + "github.com/open-component-model/ocm/pkg/version" +) + +func main() { + p := ppi.NewPlugin("cliplugin", version.Get().String()) + + p.SetShort("Demo plugin with a simple cli extension") + p.SetLong("The plugin offers the top-level command group with sub command demo") + + cmd, err := clicmd.NewCLICommand(group.New()) + if err != nil { + os.Exit(1) + } + p.RegisterCommand(cmd) + + // fmt.Printf("CMD ARGS: %v\n", os.Args[1:]) + err = cmds.NewPluginCommand(p).Execute(os.Args[1:]) + if err != nil { + os.Exit(1) + } +} diff --git a/docs/command-plugins.md b/docs/command-plugins.md new file mode 100644 index 0000000000..1ef0ebca68 --- /dev/null +++ b/docs/command-plugins.md @@ -0,0 +1,160 @@ + +The OCM Plugin framework now supports two features to +extend the CLI with new (OCM related) commands: +- definition of configuration types (consumed by the plugin) +- definition of CLI commands (for the OCM CLI) + +Additionally, it is possible to consume logging configuration from the OCM CLI for all +plugin feature commands. + +Examples see coding in `cmds/cliplugin` + +#### Config Types + +Config types are just registered at the Plugin Object; + +``` + p := ppi.NewPlugin("cliplugin", version.Get().String()) + ... + p.RegisterConfigType(configType) +``` + +The argument is just the config type as registered at the ocm library, for example: + +``` +const ConfigType = "rhabarber.config.acme.org" + +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + ... +} + +func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error { + ... +} + +func init() { + configType = cfgcpi.NewConfigType[*Config](ConfigType, usage) + cfgcpi.RegisterConfigType(configType) +} +``` + +#### CLI Commands + +CLI commands are simple configured `cobra.Command` objects. +They are registered at the plugin object with + +``` + cmd, err := clicmd.NewCLICommand(NewCommand(), clicmd.WithCLIConfig(), clicmd.WithVerb("check")) + if err != nil { + os.Exit(1) + } + p.RegisterCommand(NewCommand()) +``` + +with coding similar to + +``` + +type command struct { + date string +} + +func NewCommand() *cobra.Command { + cmd := &command{} + c := &cobra.Command{ + Use: Name + " ", + Short: "determine whether we are in rhubarb season", + Long: "The rhubarb season is between march and april.", + RunE: cmd.Run, + } + + c.Flags().StringVarP(&cmd.date, "date", "d", "", "the date to ask for (MM/DD)") + return c +} + +func (c *command) Run(cmd *cobra.Command, args []string) error { + ... +} +``` + +The plugin programming interface supports the generation of an extension command directly from a +cobra command object using the method `NewCLICommand` from the `ppi.clicmd` package. +Otherwise the `ppi.Command` interface can be implemented without requiring a cobra command.. + +If the code wants to use the config framework, for example to +- use the OCM library again +- access credentials +- get configured with declared config types + +the appropriate command feature must be set. +For the cobra support this is implemented by the option `WithCLIConfig`. +If set to true, the OCM CLI configuration is available for the config context used in the +CLI code. + +The command can be a top-level command or attached to a dedicated verb (and optionally a realm like `ocm`or `oci`). +For the cobra support this can be requested by the option `WithVerb(...)`. + +If the config framework is used just add the following anonymoud import +for an automated configuration: + +``` +import ( + // enable mandelsoft plugin logging configuration. + _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/config" +) +``` + +The plugin code is then configured with the configuration of the OCM CLI and the config framework +can be used. +If the configuration should be handled by explicit plugin code a handler can be registered with + +``` +func init() { + command.RegisterCommandConfigHandler(yourHandler) +} +``` + +It gets a config yaml according to the config objects used by the OCM library. + +#### Logging + +To get the logging configuration from the OCM CLI the plugin has be configured with + +``` + p.ForwardLogging() +``` + +If the standard mandelsoft logging from the OCM library is used the configuration can +be implemented directly with an anonymous import of + +``` +import ( + // enable mandelsoft plugin logging configuration. + _ "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/logging" +) +``` +The plugin code is then configured with the logging configuration of the OCM CLI and the mandelsoft logging frame work +can be used. +If the logging configuration should be handled by explicit plugin code a handler can be registered with + +``` +func init() { + cmds.RegisterLoggingConfigHandler(yourHandler) +} +``` + +It gets a logging configuration yaml according to the logging config used by the OCM library (`github.com/mandelsoft/logging/config`). + +#### Using Plugin command extensions from the OCM library. + +The plugin command extensions can also be called without the OCM CLI directly from the OCM library. +Therefore the plugin objects provided by the library can be used. + +Logging information and config information must explicitly be configured to be passed to the +plugin. + +Therefore the context attribute `clicfgattr.Get(ctx)` is used. It can be set via `clicfgattr.Set(...)`. +The logging configuration is extracted from the configured configuration object with target type `*logging.LoggingConfiguration`. + +If the code uses an OCM context configured with a `(ocm)utils.ConfigureXXX` function, the cli config attribute is set accordingly. diff --git a/docs/pluginreference/plugin.md b/docs/pluginreference/plugin.md index 3063d6b7ee..dfccfdc733 100644 --- a/docs/pluginreference/plugin.md +++ b/docs/pluginreference/plugin.md @@ -9,8 +9,9 @@ plugin ### Options ``` - -c, --config YAML plugin configuration - -h, --help help for plugin + -c, --config YAML plugin configuration + -h, --help help for plugin + --log-config YAML ocm logging configuration ``` ### Description @@ -71,4 +72,5 @@ apabilities of the plugin. ##### Additional Help Topics +* [plugin command](plugin_command.md) — CLI command extensions * [plugin descriptor](plugin_descriptor.md) — Plugin Descriptor Format Description diff --git a/docs/pluginreference/plugin_command.md b/docs/pluginreference/plugin_command.md new file mode 100644 index 0000000000..57d91820db --- /dev/null +++ b/docs/pluginreference/plugin_command.md @@ -0,0 +1,19 @@ +## plugin command — CLI Command Extensions + +### Description + +This command group provides all CLI command extensions +described by an access method descriptor ([plugin descriptor](plugin_descriptor.md). + +### SEE ALSO + +##### Parents + +* [plugin](plugin.md) — OCM Plugin + + + +##### Additional Links + +* [plugin descriptor](plugin_descriptor.md) — Plugin Descriptor Format Description + diff --git a/docs/reference/ocm.md b/docs/reference/ocm.md index d2e3a12be2..70560cc8ce 100644 --- a/docs/reference/ocm.md +++ b/docs/reference/ocm.md @@ -16,6 +16,7 @@ ocm [] ... -C, --cred stringArray credential setting -h, --help help for ocm -I, --issuer stringArray issuer name or distinguished name (DN) (optionally for dedicated signature) ([:=] + --logJson log as json instead of human readable logs --logconfig string log config -L, --logfile string set log file --logkeys stringArray log tags/realms(with leading /) to be enabled ([/[+]]name{,[/[+]]name}[=level]) @@ -117,7 +118,7 @@ an @, the logging configuration is taken from a file. The value can be a simple type or a JSON/YAML string for complex values (see [ocm attributes](ocm_attributes.md). The following attributes are supported: -- github.com/mandelsoft/logforward: *logconfig* Logging config structure used for config forwarding +- github.com/mandelsoft/logforward [logfwd]: *logconfig* Logging config structure used for config forwarding This attribute is used to specify a logging configuration intended to be forwarded to other tools. @@ -240,7 +241,7 @@ The value can be a simple type or a JSON/YAML string for complex values Directory to look for OCM plugin executables. -- github.com/mandelsoft/ocm/rootcerts: *JSON* +- github.com/mandelsoft/ocm/rootcerts [rootcerts]: *JSON* General root certificate settings given as JSON document with the following format: @@ -292,6 +293,10 @@ The value can be a simple type or a JSON/YAML string for complex values The are temporarily stored in the filesystem, instead of the memory, to avoid blowing up the memory consumption. +- ocm.software/cliconfig [cliconfig]: *cliconfigr* Configuration Object passed to command line pluging. + + + - ocm.software/compositionmode [compositionmode]: *bool* (default: false Composition mode decouples a component version provided by a repository diff --git a/docs/reference/ocm_attributes.md b/docs/reference/ocm_attributes.md index 2e18ff693f..48124fd901 100644 --- a/docs/reference/ocm_attributes.md +++ b/docs/reference/ocm_attributes.md @@ -10,7 +10,7 @@ command line options of the main command (see [ocm](ocm.md)). The following options are available in the currently used version of the OCM library: -- github.com/mandelsoft/logforward: *logconfig* Logging config structure used for config forwarding +- github.com/mandelsoft/logforward [logfwd]: *logconfig* Logging config structure used for config forwarding This attribute is used to specify a logging configuration intended to be forwarded to other tools. @@ -133,7 +133,7 @@ OCM library: Directory to look for OCM plugin executables. -- github.com/mandelsoft/ocm/rootcerts: *JSON* +- github.com/mandelsoft/ocm/rootcerts [rootcerts]: *JSON* General root certificate settings given as JSON document with the following format: @@ -185,6 +185,10 @@ OCM library: The are temporarily stored in the filesystem, instead of the memory, to avoid blowing up the memory consumption. +- ocm.software/cliconfig [cliconfig]: *cliconfigr* Configuration Object passed to command line pluging. + + + - ocm.software/compositionmode [compositionmode]: *bool* (default: false Composition mode decouples a component version provided by a repository diff --git a/docs/reference/ocm_configfile.md b/docs/reference/ocm_configfile.md index 2265aebee5..2e28cd951f 100644 --- a/docs/reference/ocm_configfile.md +++ b/docs/reference/ocm_configfile.md @@ -25,6 +25,16 @@ The following configuration types are supported: <name>: <yaml defining the attribute> ... +- cli.ocm.config.ocm.software + The config type cli.ocm.config.ocm.software is used to handle the + main configuration flags of the OCM command line tool. + +
+      type: cli.ocm.config.ocm.software
+      aliases:
+         <name>: <OCI registry specification>
+         ...
+  
- credentials.config.ocm.software The config type credentials.config.ocm.software can be used to define a list of arbitrary configuration specifications: diff --git a/docs/reference/ocm_get.md b/docs/reference/ocm_get.md index 7d2dd01b2e..5c5149a7d9 100644 --- a/docs/reference/ocm_get.md +++ b/docs/reference/ocm_get.md @@ -23,6 +23,7 @@ ocm get [] ... * [ocm get artifacts](ocm_get_artifacts.md) — get artifact version * [ocm get componentversions](ocm_get_componentversions.md) — get component version +* [ocm get config](ocm_get_config.md) — Get evaluated config for actual command call * [ocm get credentials](ocm_get_credentials.md) — Get credentials for a dedicated consumer spec * [ocm get plugins](ocm_get_plugins.md) — get plugins * [ocm get references](ocm_get_references.md) — get references of a component version diff --git a/docs/reference/ocm_get_config.md b/docs/reference/ocm_get_config.md new file mode 100644 index 0000000000..16f0b8ba58 --- /dev/null +++ b/docs/reference/ocm_get_config.md @@ -0,0 +1,45 @@ +## ocm get config — Get Evaluated Config For Actual Command Call + +### Synopsis + +``` +ocm get config +``` + +##### Aliases + +``` +config, cfg +``` + +### Options + +``` + -h, --help help for config + -O, --outfile string output file or directory + -o, --output string output mode (JSON, json, yaml) +``` + +### Description + + +Evaluate the command line arguments and all explicitly +or implicitly used configuration files and provide +a single configuration object. + + +With the option --output the output mode can be selected. +The following modes are supported: + - (default) + - JSON + - json + - yaml + + +### SEE ALSO + +##### Parents + +* [ocm get](ocm_get.md) — Get information about artifacts and components +* [ocm](ocm.md) — Open Component Model command line client + diff --git a/go.mod b/go.mod index 190a6e17cb..52c76e2b95 100644 --- a/go.mod +++ b/go.mod @@ -36,11 +36,12 @@ require ( github.com/google/go-github/v45 v45.2.0 github.com/hashicorp/vault-client-go v0.4.3 github.com/imdario/mergo v0.3.16 + github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b github.com/klauspost/compress v1.17.9 github.com/klauspost/pgzip v1.2.6 github.com/mandelsoft/filepath v0.0.0-20240223090642-3e2777258aa3 - github.com/mandelsoft/goutils v0.0.0-20240608132424-ec9fb7fa611a - github.com/mandelsoft/logging v0.0.0-20240201091719-67180059d6bf + github.com/mandelsoft/goutils v0.0.0-20240623134558-383cb09dec16 + github.com/mandelsoft/logging v0.0.0-20240618075559-fdca28a87b0a github.com/mandelsoft/spiff v1.7.0-beta-5 github.com/mandelsoft/vfs v0.4.3 github.com/marstr/guid v1.1.0 diff --git a/go.sum b/go.sum index 0782928600..15f555bfc2 100644 --- a/go.sum +++ b/go.sum @@ -640,6 +640,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b h1:FQ7+9fxhyp82ks9vAuyPzG0/vVbWwMwLJ+P6yJI5FN8= +github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b/go.mod h1:HMcgvsgd0Fjj4XXDkbjdmlbI505rUPBs6WBMYg2pXks= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -682,10 +684,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mandelsoft/filepath v0.0.0-20240223090642-3e2777258aa3 h1:oo9nIgnyiBgYPbcZslRT4y29siuL5EoNJ/t1tr0xEVQ= github.com/mandelsoft/filepath v0.0.0-20240223090642-3e2777258aa3/go.mod h1:LxhqC7khDoRENwooP6f/vWvia9ivj6TqLYrR39zqkN0= -github.com/mandelsoft/goutils v0.0.0-20240608132424-ec9fb7fa611a h1:7ZnEdaPeoshk2I1GfZcE51NkSjGwfuVoM2Jf/tAXB2w= -github.com/mandelsoft/goutils v0.0.0-20240608132424-ec9fb7fa611a/go.mod h1:9TJgkwSY43RWHiIAAz7fL8SEIHf0L13Pk4w8fDIt+i4= -github.com/mandelsoft/logging v0.0.0-20240201091719-67180059d6bf h1:WEmgzeArDbp6Aw34jmziMIE5ygo2zpl/atXRq3D7lSw= -github.com/mandelsoft/logging v0.0.0-20240201091719-67180059d6bf/go.mod h1:uO460C1lIB3IOOgrbXhAlz3AKsOv4T2K6ALBn3PwuSg= +github.com/mandelsoft/goutils v0.0.0-20240623134558-383cb09dec16 h1:7tcgfj+QZSfABuZKc9PrgQj1U+A7MsRySCG4ZG5JvLg= +github.com/mandelsoft/goutils v0.0.0-20240623134558-383cb09dec16/go.mod h1:9TJgkwSY43RWHiIAAz7fL8SEIHf0L13Pk4w8fDIt+i4= +github.com/mandelsoft/logging v0.0.0-20240618075559-fdca28a87b0a h1:MAvh0gbP2uwKmf7wWCkYCzrYa6vPjBvYeGhoUlVHwtI= +github.com/mandelsoft/logging v0.0.0-20240618075559-fdca28a87b0a/go.mod h1:uO460C1lIB3IOOgrbXhAlz3AKsOv4T2K6ALBn3PwuSg= github.com/mandelsoft/spiff v1.7.0-beta-5 h1:3kC10nTviDQhL8diSxp7i4IC2iSiDg6KPbH1CAq7Lfw= github.com/mandelsoft/spiff v1.7.0-beta-5/go.mod h1:TwEeOPuRZxlzQBCLEyVTlHmBSruSGGNdiQ2fovVJ8ao= github.com/mandelsoft/vfs v0.4.3 h1:2UMrxQkMXkcHyuqSFhgFDupQ1fmqpKLZuu04DOHx1PA= diff --git a/pkg/cobrautils/funcs.go b/pkg/cobrautils/funcs.go index 4810e3d5a7..965398a912 100644 --- a/pkg/cobrautils/funcs.go +++ b/pkg/cobrautils/funcs.go @@ -1,9 +1,12 @@ package cobrautils import ( + "fmt" "sort" + "strconv" "strings" + "github.com/mandelsoft/goutils/sliceutils" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/text/cases" @@ -13,6 +16,8 @@ import ( ) var templatefuncs = map[string]interface{}{ + "useLine": useLine, + "commandPath": commandPath, "indent": indent, "skipCommand": skipCommand, "soleCommand": soleCommand, @@ -22,6 +27,52 @@ var templatefuncs = map[string]interface{}{ "commandList": commandList, } +const COMMAND_PATH_SUBSTITUTION = "ocm.software/commandPathSubstitution" + +func SetCommandSubstitutionForTree(cmd *cobra.Command, remove int, prepend []string) { + SetCommandSubstitution(cmd, remove, prepend) + for _, c := range cmd.Commands() { + SetCommandSubstitutionForTree(c, remove, prepend) + } +} + +func SetCommandSubstitution(cmd *cobra.Command, remove int, prepend []string) { + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + cmd.Annotations[COMMAND_PATH_SUBSTITUTION] = fmt.Sprintf("%d:%s", remove, strings.Join(prepend, " ")) +} + +func useLine(c *cobra.Command) string { + cp := commandPath(c) + i := strings.Index(c.Use, " ") + if i > 0 { + cp += c.Use[i:] + } + if !c.DisableFlagsInUseLine && c.HasAvailableFlags() && !strings.Contains(cp, "[flags]") { + cp += " [flags]" + } + return cp +} + +func commandPath(c *cobra.Command) string { + if c.Annotations != nil { + subst := c.Annotations[COMMAND_PATH_SUBSTITUTION] + if subst != "" { + i := strings.Index(subst, ":") + if i > 0 { + remove, err := strconv.Atoi(subst[:i]) + if err == nil { + fields := strings.Split(c.CommandPath(), " ") + fields = sliceutils.CopyAppend(strings.Split(subst[i+1:], " "), fields[remove:]...) + return strings.Join(fields, " ") + } + } + } + } + return c.CommandPath() +} + func flagUsages(fs *pflag.FlagSet) string { return groups.FlagUsagesWrapped(fs, 0) } diff --git a/pkg/cobrautils/logopts/close_test.go b/pkg/cobrautils/logopts/close_test.go new file mode 100644 index 0000000000..d3dcf33087 --- /dev/null +++ b/pkg/cobrautils/logopts/close_test.go @@ -0,0 +1,55 @@ +package logopts + +import ( + "runtime" + "time" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/logging" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + + logging2 "github.com/open-component-model/ocm/pkg/cobrautils/logopts/logging" + "github.com/open-component-model/ocm/pkg/contexts/datacontext" + "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/vfsattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm" +) + +var _ = Describe("log file", func() { + var fs vfs.FileSystem + + BeforeEach(func() { + fs = Must(osfs.NewTempFileSystem()) + }) + + AfterEach(func() { + vfs.Cleanup(fs) + }) + + It("closes log file", func() { + ctx := ocm.New(datacontext.MODE_INITIAL) + lctx := logging.NewDefault() + + vfsattr.Set(ctx, fs) + + opts := &Options{ + ConfigFragment: ConfigFragment{ + LogLevel: "debug", + LogFileName: "debug.log", + }, + } + + MustBeSuccessful(opts.Configure(ctx, lctx)) + + Expect(logging2.GetLogFileFor(opts.LogFileName, fs)).NotTo(BeNil()) + lctx = nil + for i := 1; i < 100; i++ { + time.Sleep(1 * time.Millisecond) + runtime.GC() + } + Expect(logging2.GetLogFileFor(opts.LogFileName, fs)).To(BeNil()) + }) +}) diff --git a/pkg/cobrautils/logopts/config.go b/pkg/cobrautils/logopts/config.go new file mode 100644 index 0000000000..d4738f6e31 --- /dev/null +++ b/pkg/cobrautils/logopts/config.go @@ -0,0 +1,146 @@ +package logopts + +import ( + "strings" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/logging" + "github.com/mandelsoft/logging/config" + "github.com/mandelsoft/logging/logrusl/adapter" + "github.com/mandelsoft/logging/logrusr" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + + logdata "github.com/open-component-model/ocm/pkg/cobrautils/logopts/logging" + "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/logforward" + "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/vfsattr" + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/utils" +) + +// ConfigFragment is a serializable log config used +// for CLI commands. +type ConfigFragment struct { + LogLevel string `json:"logLevel,omitempty"` + LogConfig string `json:"logConfig,omitempty"` + LogKeys []string `json:"logKeys,omitempty"` + Json bool `json:"json,omitempty"` + + // LogFileName is a CLI option, only. Do not serialize and forward + LogFileName string `json:"-"` +} + +func (c *ConfigFragment) AddFlags(fs *pflag.FlagSet) { + fs.BoolVarP(&c.Json, "logJson", "", false, "log as json instead of human readable logs") + fs.StringVarP(&c.LogLevel, "loglevel", "l", "", "set log level") + fs.StringVarP(&c.LogFileName, "logfile", "L", "", "set log file") + fs.StringVarP(&c.LogConfig, "logconfig", "", "", "log config") + fs.StringArrayVarP(&c.LogKeys, "logkeys", "", nil, "log tags/realms(with leading /) to be enabled ([/[+]]name{,[/[+]]name}[=level])") +} + +func (c *ConfigFragment) GetLogConfig(fss ...vfs.FileSystem) (*config.Config, error) { + var ( + err error + cfg *config.Config + ) + + if c.LogConfig != "" { + var data []byte + if strings.HasPrefix(c.LogConfig, "@") { + data, err = utils.ReadFile(c.LogConfig[1:], utils.FileSystem(fss...)) + if err != nil { + return nil, errors.Wrapf(err, "cannot read logging config file %q", c.LogConfig[1:]) + } + } else { + data = []byte(c.LogConfig) + } + if cfg, err = config.EvaluateFromData(data); err != nil { + return nil, errors.Wrapf(err, "invalid logging config: %q", c.LogConfig) + } + } else { + cfg = &config.Config{DefaultLevel: "Warn"} + } + + for _, t := range c.LogKeys { + level := logging.InfoLevel + i := strings.Index(t, "=") + if i >= 0 { + level, err = logging.ParseLevel(t[i+1:]) + if err != nil { + return nil, errors.Wrapf(err, "invalid log tag setting") + } + t = t[:i] + } + var cfgcond []config.Condition + + for _, tag := range strings.Split(t, ",") { + tag = strings.TrimSpace(tag) + if strings.HasPrefix(tag, "/") { + realm := tag[1:] + if strings.HasPrefix(realm, "+") { + cfgcond = append(cfgcond, config.RealmPrefix(realm[1:])) + } else { + cfgcond = append(cfgcond, config.Realm(realm)) + } + } else { + cfgcond = append(cfgcond, config.Tag(tag)) + } + } + cfg.Rules = append(cfg.Rules, config.ConditionalRule(logging.LevelName(level), cfgcond...)) + } + + if c.LogLevel != "" { + _, err := logging.ParseLevel(c.LogLevel) + if err != nil { + return nil, errors.Wrapf(err, "invalid log level %q", c.LogLevel) + } + cfg.DefaultLevel = c.LogLevel + } + + return cfg, nil +} + +func (c *ConfigFragment) Evaluate(ctx ocm.Context, logctx logging.Context, main bool) (*EvaluatedOptions, error) { + var err error + var opts EvaluatedOptions + + for logctx.Tree().GetBaseContext() != nil { + logctx = logctx.Tree().GetBaseContext() + } + + fs := vfsattr.Get(ctx) + if main && c.LogFileName != "" && logdata.GlobalLogFileOverride == "" { + if opts.LogFile == nil { + opts.LogFile, err = logdata.LogFileFor(c.LogFileName, fs) + if err != nil { + return nil, errors.Wrapf(err, "cannot open log file %q", opts.LogFile) + } + } + logdata.ConfigureLogrusFor(logctx, !c.Json, opts.LogFile) + if logctx == logging.DefaultContext() { + logdata.GlobalLogFile = opts.LogFile + } + } else { + // overwrite current log formatter in case of a logrus logger is + // used as logging backend. + var f logrus.Formatter = adapter.NewJSONFormatter() + if !c.Json { + f = adapter.NewTextFmtFormatter() + } + logrusr.SetFormatter(logging.UnwrapLogSink(logctx.GetSink()), f) + } + + cfg, err := c.GetLogConfig(fs) + if err != nil { + return &opts, err + } + err = config.Configure(logctx, cfg) + if err != nil { + return &opts, err + } + opts.LogForward = cfg + logforward.Set(ctx.AttributesContext(), opts.LogForward) + + return &opts, nil +} diff --git a/pkg/cobrautils/logopts/doc.go b/pkg/cobrautils/logopts/doc.go new file mode 100644 index 0000000000..2f1abdf6ba --- /dev/null +++ b/pkg/cobrautils/logopts/doc.go @@ -0,0 +1,4 @@ +// Package logopts is used for CLI options used to control the logging, globally or for +// a dedicated context. +// If used the main program should call logopts.CloseLogFiles() before exiting. +package logopts diff --git a/pkg/cobrautils/logopts/logging/config.go b/pkg/cobrautils/logopts/logging/config.go new file mode 100644 index 0000000000..bc6812d423 --- /dev/null +++ b/pkg/cobrautils/logopts/logging/config.go @@ -0,0 +1,61 @@ +package logging + +import ( + "runtime" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/logging" + logcfg "github.com/mandelsoft/logging/config" + "github.com/mandelsoft/logging/logrusl" + "github.com/mandelsoft/logging/logrusl/adapter" + "github.com/mandelsoft/logging/logrusr" + "github.com/mandelsoft/logging/utils" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/sirupsen/logrus" +) + +// LoggingConfiguration describes logging configuration for a slave executables like +// plugins. +type LoggingConfiguration struct { + LogFileName string `json:"logFileName"` + LogConfig logcfg.Config `json:"logConfig"` + Json bool `json:"json,omitempty"` +} + +func (c *LoggingConfiguration) Apply() error { + if GlobalLogFileOverride == c.LogFileName { + return nil + } + logctx := logging.DefaultContext() + if c.LogFileName != "" { + logfile, err := LogFileFor(c.LogFileName, osfs.OsFs) + if err != nil { + return errors.Wrapf(err, "cannot open log file %q", c.LogFileName) + } + ConfigureLogrusFor(logctx, false, logfile) + GlobalLogFile = logfile + GlobalLogFileOverride = c.LogFileName + } else { + // overwrite current log formatter in case of a logrus logger is + // used as logging backend. + var f logrus.Formatter = adapter.NewJSONFormatter() + if !c.Json { + f = adapter.NewTextFmtFormatter() + } + logrusr.SetFormatter(logging.UnwrapLogSink(logctx.GetSink()), f) + } + return nil +} + +func ConfigureLogrusFor(logctx logging.Context, human bool, logfile *LogFile) { + settings := logrusl.Adapter().WithWriter(utils.NewSyncWriter(logfile.File())) + if human { + settings = settings.Human() + } else { + settings = settings.WithFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"}) + } + + log := settings.NewLogrus() + logctx.SetBaseLogger(logrusr.New(log)) + runtime.SetFinalizer(log, func(_ *logrus.Logger) { logfile.Close() }) +} diff --git a/pkg/cobrautils/logopts/logging/global.go b/pkg/cobrautils/logopts/logging/global.go new file mode 100644 index 0000000000..8351c95170 --- /dev/null +++ b/pkg/cobrautils/logopts/logging/global.go @@ -0,0 +1,6 @@ +package logging + +var ( + GlobalLogFile *LogFile + GlobalLogFileOverride string +) diff --git a/pkg/cobrautils/logopts/logging/logfiles.go b/pkg/cobrautils/logopts/logging/logfiles.go new file mode 100644 index 0000000000..cd93dce4cc --- /dev/null +++ b/pkg/cobrautils/logopts/logging/logfiles.go @@ -0,0 +1,91 @@ +package logging + +import ( + "os" + "sync" + + "github.com/mandelsoft/goutils/sliceutils" + "github.com/mandelsoft/vfs/pkg/vfs" + "golang.org/x/exp/slices" +) + +type LogFile struct { + count int + path string + file vfs.File + fs vfs.FileSystem +} + +func (l *LogFile) File() vfs.File { + return l.file +} + +func (l *LogFile) Close() error { + lock.Lock() + defer lock.Unlock() + + l.count-- + if l.count == 0 { + i := slices.Index(logFiles, l) + if i >= 0 { + logFiles.DeleteIndex(i) + } + return l.file.Close() + } + return nil +} + +var ( + lock sync.Mutex + logFiles sliceutils.Slice[*LogFile] +) + +func CloseLogFiles() { + lock.Lock() + defer lock.Unlock() + + for _, f := range logFiles { + f.file.Close() + } + logFiles = nil +} + +func LogFileFor(path string, fs vfs.FileSystem) (*LogFile, error) { + lock.Lock() + defer lock.Unlock() + + path, f := getLogFileFor(path, fs) + if f != nil { + f.count++ + return f, nil + } + lf, err := fs.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { + return nil, err + } + f = &LogFile{path: path, file: lf, fs: fs, count: 1} + logFiles.Add(f) + return f, nil +} + +func getLogFileFor(path string, fs vfs.FileSystem) (string, *LogFile) { + path, err := vfs.Canonical(fs, path, false) + if err != nil { + return path, nil + } + + for _, f := range logFiles { + if f.path == path && f.fs == fs { + return path, f + } + } + return path, nil +} + +func GetLogFileFor(path string, fs vfs.FileSystem) *LogFile { + lock.Lock() + defer lock.Unlock() + + _, l := getLogFileFor(path, fs) + return l +} diff --git a/pkg/cobrautils/logopts/options.go b/pkg/cobrautils/logopts/options.go index c848194fcf..0a4b3c9310 100644 --- a/pkg/cobrautils/logopts/options.go +++ b/pkg/cobrautils/logopts/options.go @@ -1,20 +1,12 @@ package logopts import ( - "strings" - - "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/logging" "github.com/mandelsoft/logging/config" - "github.com/mandelsoft/logging/logrusr" - "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/sirupsen/logrus" "github.com/spf13/pflag" - "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/logforward" - "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/vfsattr" + logging2 "github.com/open-component-model/ocm/pkg/cobrautils/logopts/logging" "github.com/open-component-model/ocm/pkg/contexts/ocm" - "github.com/open-component-model/ocm/pkg/utils" ) var Description = ` @@ -41,114 +33,39 @@ logging configuration (yaml/json) via command line. If the argument starts with an @, the logging configuration is taken from a file. ` -type Options struct { - LogLevel string - LogFileName string - LogConfig string - LogKeys []string +//////////////////////////////////////////////////////////////////////////////// - LogFile vfs.File +type EvaluatedOptions struct { LogForward *config.Config + LogFile *logging2.LogFile } -func (o *Options) AddFlags(fs *pflag.FlagSet) { - fs.StringVarP(&o.LogLevel, "loglevel", "l", "", "set log level") - fs.StringVarP(&o.LogFileName, "logfile", "L", "", "set log file") - fs.StringVarP(&o.LogConfig, "logconfig", "", "", "log config") - fs.StringArrayVarP(&o.LogKeys, "logkeys", "", nil, "log tags/realms(with leading /) to be enabled ([/[+]]name{,[/[+]]name}[=level])") -} - -func (o *Options) Close() error { +func (o *EvaluatedOptions) Close() error { if o.LogFile == nil { return nil } return o.LogFile.Close() } -func (o *Options) Configure(ctx ocm.Context, logctx logging.Context) error { - var err error - - if logctx == nil { - // by default: always configure the root logging context used for the actual ocm context. - logctx = ctx.LoggingContext() - for logctx.Tree().GetBaseContext() != nil { - logctx = logctx.Tree().GetBaseContext() - } - } - - if o.LogLevel != "" { - l, err := logging.ParseLevel(o.LogLevel) - if err != nil { - return errors.Wrapf(err, "invalid log level %q", o.LogLevel) - } - logctx.SetDefaultLevel(l) - } else { - logctx.SetDefaultLevel(logging.WarnLevel) - } - logcfg := &config.Config{DefaultLevel: logging.LevelName(logctx.GetDefaultLevel())} +type Options struct { + ConfigFragment + *EvaluatedOptions +} - fs := vfsattr.Get(ctx) - if o.LogFileName != "" { - o.LogFile, err = fs.OpenFile(o.LogFileName, vfs.O_CREATE|vfs.O_WRONLY, 0o600) - if err != nil { - return errors.Wrapf(err, "cannot open log file %q", o.LogFile) - } - log := logrus.New() - log.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"}) - log.SetOutput(o.LogFile) - logctx.SetBaseLogger(logrusr.New(log)) - } +func (o *Options) AddFlags(fs *pflag.FlagSet) { + o.ConfigFragment.AddFlags(fs) +} - if o.LogConfig != "" { - var cfg []byte - if strings.HasPrefix(o.LogConfig, "@") { - cfg, err = utils.ReadFile(o.LogConfig[1:], fs) - if err != nil { - return errors.Wrapf(err, "cannot read logging config file %q", o.LogConfig[1:]) - } - } else { - cfg = []byte(o.LogConfig) - } - if err = config.ConfigureWithData(logctx, cfg); err != nil { - return errors.Wrapf(err, "invalid logging config: %q", o.LogFile) - } +func (o *Options) Close() error { + if o.EvaluatedOptions == nil { + return nil } + return o.EvaluatedOptions.Close() +} - for _, t := range o.LogKeys { - level := logging.InfoLevel - i := strings.Index(t, "=") - if i >= 0 { - level, err = logging.ParseLevel(t[i+1:]) - if err != nil { - return errors.Wrapf(err, "invalid log tag setting") - } - t = t[:i] - } - var cond []logging.Condition - var cfgcond []config.Condition - - for _, tag := range strings.Split(t, ",") { - tag = strings.TrimSpace(tag) - if strings.HasPrefix(tag, "/") { - realm := tag[1:] - if strings.HasPrefix(realm, "+") { - cond = append(cond, logging.NewRealmPrefix(realm[1:])) - cfgcond = append(cfgcond, config.RealmPrefix(realm[1:])) - } else { - cond = append(cond, logging.NewRealm(realm)) - cfgcond = append(cfgcond, config.Realm(realm)) - } - } else { - cond = append(cond, logging.NewTag(tag)) - cfgcond = append(cfgcond, config.Tag(tag)) - } - } - rule := logging.NewConditionRule(level, cond...) - logcfg.Rules = append(logcfg.Rules, config.ConditionalRule(logging.LevelName(level), cfgcond...)) - logctx.AddRule(rule) - } - o.LogForward = logcfg - logforward.Set(ctx.AttributesContext(), logcfg) +func (o *Options) Configure(ctx ocm.Context, logctx logging.Context) error { + var err error - return nil + o.EvaluatedOptions, err = o.ConfigFragment.Evaluate(ctx, logctx, true) + return err } diff --git a/pkg/cobrautils/logopts/options_test.go b/pkg/cobrautils/logopts/options_test.go index e10fe0aa75..da67262d00 100644 --- a/pkg/cobrautils/logopts/options_test.go +++ b/pkg/cobrautils/logopts/options_test.go @@ -1,14 +1,14 @@ package logopts import ( + "encoding/json" "fmt" . "github.com/mandelsoft/goutils/testutils" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/mandelsoft/logging" "github.com/mandelsoft/logging/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "sigs.k8s.io/yaml" "github.com/open-component-model/ocm/pkg/contexts/clictx" @@ -19,11 +19,13 @@ var _ = Describe("log configuration", func() { ctx := clictx.New() opts := Options{ - LogLevel: "debug", - LogKeys: []string{ - "tag=trace", - "/realm=info", - "/+all=info", + ConfigFragment: ConfigFragment{ + LogLevel: "debug", + LogKeys: []string{ + "tag=trace", + "/realm=info", + "/+all=info", + }, }, } @@ -44,4 +46,13 @@ var _ = Describe("log configuration", func() { Expect(logctx.Logger(logging.NewRealm("realm/test")).Enabled(logging.InfoLevel)).To(BeTrue()) Expect(logctx.Logger(logging.NewRealm("realm/test")).Enabled(logging.DebugLevel)).To(BeTrue()) }) + + Context("serialize", func() { + It("does not serialize log file name", func() { + var c ConfigFragment + c.LogFileName = "test" + data := Must(json.Marshal(&c)) + Expect(string(data)).To(Equal("{}")) + }) + }) }) diff --git a/pkg/cobrautils/template.go b/pkg/cobrautils/template.go index eb84dbfeb0..76e12ea251 100644 --- a/pkg/cobrautils/template.go +++ b/pkg/cobrautils/template.go @@ -1,10 +1,10 @@ package cobrautils -const HelpTemplate = "{{.CommandPath}} \u2014 {{title .Short}}" + `{{if .IsAvailableCommand}} +const HelpTemplate = "{{commandPath .}} \u2014 {{title .Short}}" + `{{if .IsAvailableCommand}} Synopsis:{{if .Runnable}} - {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} - {{if or .Runnable (soleCommand .Use)}}{{if .HasAvailableLocalFlags}}{{.CommandPath}} [] ...{{else}}{{.CommandPath}} ...{{end}}{{else}}{{.UseLine}}{{end}}{{end}}{{if gt (len .Aliases) 0}} + {{useLine .}}{{end}}{{if .HasAvailableSubCommands}} + {{if or .Runnable (soleCommand .Use)}}{{if .HasAvailableLocalFlags}}{{commandPath .}} [] ...{{else}}{{commandPath .}} ...{{end}}{{else}}{{useLine .}}{{end}}{{end}}{{if gt (len .Aliases) 0}} Aliases: {{.NameAndAliases}}{{end}}{{end}}{{if .HasAvailableSubCommands}} @@ -20,20 +20,20 @@ Global Flags: Description: {{with (or .Long .Short)}}{{. | substituteCommandLinks | trimTrailingWhitespaces | indent 2}}{{end}}{{if .HasAvailableSubCommands}} - Use {{.CommandPath}} -h for additional help. + Use {{commandPath .}} -h for additional help. {{end}}{{if .HasExample}} Examples: {{.Example | indent 2}}{{end}}{{if .HasHelpSubCommands}} Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + {{rpad (commandPath .) .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} {{end}} ` const UsageTemplate = `Synopsis:{{if .Runnable}} - {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}{{if .HasAvailableLocalFlags}} - {{.CommandPath}} [] ...{{else}}{{.CommandPath}} ...{{end}}{{end}}{{if gt (len .Aliases) 0}} + {{useLine .}}{{end}}{{if .HasAvailableSubCommands}}{{if .HasAvailableLocalFlags}} + {{commandPath .}} [] ...{{else}}{{commandPath .}} ...{{end}}{{end}}{{if gt (len .Aliases) 0}} Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}} @@ -51,7 +51,7 @@ Global Flags: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + {{rpad (commandPath .) .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +Use "{{commandPath .}} [command] --help" for more information about a command.{{end}} ` diff --git a/pkg/cobrautils/tweak.go b/pkg/cobrautils/tweak.go index 59948423e0..5501fcfba6 100644 --- a/pkg/cobrautils/tweak.go +++ b/pkg/cobrautils/tweak.go @@ -1,6 +1,7 @@ package cobrautils import ( + "fmt" "regexp" "strings" @@ -11,8 +12,9 @@ import ( "github.com/open-component-model/ocm/pkg/out" ) -func TweakCommand(cmd *cobra.Command, ctx out.Context) { +func TweakCommand(cmd *cobra.Command, ctx out.Context) *cobra.Command { if ctx != nil { + cmd.UseLine() cmd.SetOut(ctx.StdOut()) cmd.SetErr(ctx.StdErr()) cmd.SetIn(ctx.StdIn()) @@ -22,6 +24,7 @@ func TweakCommand(cmd *cobra.Command, ctx out.Context) { SupportNestedHelpFunc(cmd) cmd.SetHelpTemplate(HelpTemplate) cmd.SetUsageTemplate(UsageTemplate) + return cmd } // SupportNestedHelpFunc adds support help evaluation of given nested command path. @@ -127,3 +130,64 @@ func CleanMarkdown(s string) string { } return strings.Join(r, "\n") } + +func GetHelpCommand(cmd *cobra.Command) *cobra.Command { + for _, c := range cmd.Commands() { + if c.Name() == "help" { + return c + } + } + return nil +} + +// TweakHelpCommandFor generates a help command similar to the default cobra one, +// which forwards the additional arguments to the help function. +func TweakHelpCommandFor(c *cobra.Command) *cobra.Command { + c.InitDefaultHelpCmd() + defhelp := GetHelpCommand(c) + c.SetHelpCommand(nil) + c.RemoveCommand(defhelp) + + var help *cobra.Command + help = &cobra.Command{ + Use: "help [command]", + Short: "Help about any command", + Long: `Help provides help for any command in the application. +Simply type ` + c.Name() + ` help [path to command] for full details.`, + ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var completions []string + cmd, _, e := c.Root().Find(args) + if e != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if cmd == nil { + // Root help command. + cmd = c.Root() + } + for _, subCmd := range cmd.Commands() { + if subCmd.IsAvailableCommand() || subCmd == help { + if strings.HasPrefix(subCmd.Name(), toComplete) { + completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short)) + } + } + } + return completions, cobra.ShellCompDirectiveNoFileComp + }, + Run: func(c *cobra.Command, args []string) { + cmd, subargs, e := c.Parent().Find(args) + if cmd == nil || e != nil { + c.Printf("Unknown help topic %#q\n", args) + cobra.CheckErr(c.Root().Usage()) + } else { + cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown + cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown + cmd.HelpFunc()(cmd, subargs) + } + }, + GroupID: defhelp.GroupID, + } + + c.SetHelpCommand(help) + c.AddCommand(help) + return help +} diff --git a/pkg/cobrautils/utils.go b/pkg/cobrautils/utils.go new file mode 100644 index 0000000000..dc2e4fd280 --- /dev/null +++ b/pkg/cobrautils/utils.go @@ -0,0 +1,14 @@ +package cobrautils + +import ( + "github.com/spf13/cobra" +) + +func Find(cmd *cobra.Command, name string) *cobra.Command { + for _, c := range cmd.Commands() { + if c.Name() == name { + return c + } + } + return nil +} diff --git a/pkg/contexts/config/config/utils.go b/pkg/contexts/config/config/utils.go new file mode 100644 index 0000000000..126ebdb7f2 --- /dev/null +++ b/pkg/contexts/config/config/utils.go @@ -0,0 +1,60 @@ +package config + +import ( + "github.com/open-component-model/ocm/pkg/contexts/config/cpi" +) + +type Aggregator struct { + cfg cpi.Config + aggr *Config + optimized bool +} + +func NewAggregator(optimized bool, cfgs ...cpi.Config) (*Aggregator, error) { + a := &Aggregator{optimized: optimized} + for _, c := range cfgs { + err := a.AddConfig(c) + if err != nil { + return nil, err + } + } + return a, nil +} + +func (a *Aggregator) Get() cpi.Config { + return a.cfg +} + +func (a *Aggregator) AddConfig(cfg cpi.Config) error { + if a.cfg == nil { + a.cfg = cfg + if aggr, ok := cfg.(*Config); ok && a.optimized { + a.aggr = aggr + } + } else { + if a.aggr == nil { + a.aggr = New() + if m, ok := a.cfg.(*Config); ok { + // transfer initial config aggregation + for _, c := range m.Configurations { + err := a.aggr.AddConfig(c) + if err != nil { + return err + } + } + } else { + // add initial config to new aggregation + err := a.aggr.AddConfig(a.cfg) + if err != nil { + return err + } + } + a.cfg = a.aggr + } + err := a.aggr.AddConfig(cfg) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/contexts/config/cpi/interface.go b/pkg/contexts/config/cpi/interface.go index 68b019ba70..b61ab01eb6 100644 --- a/pkg/contexts/config/cpi/interface.go +++ b/pkg/contexts/config/cpi/interface.go @@ -54,11 +54,11 @@ func IsGeneric(cfg Config) bool { type Updater = internal.Updater -func NewUpdater(ctx Context, target interface{}) Updater { +func NewUpdater(ctx ContextProvider, target interface{}) Updater { return internal.NewUpdater(ctx, target) } -func NewUpdaterForFactory[T any](ctx Context, f func() T) Updater { +func NewUpdaterForFactory[T any](ctx ContextProvider, f func() T) Updater { return internal.NewUpdaterForFactory(ctx, f) } diff --git a/pkg/contexts/config/internal/configtypes.go b/pkg/contexts/config/internal/configtypes.go index 14422d3128..ac03819529 100644 --- a/pkg/contexts/config/internal/configtypes.go +++ b/pkg/contexts/config/internal/configtypes.go @@ -146,6 +146,8 @@ func (s *GenericConfig) Evaluate(ctx Context) (Config, error) { if IsGeneric(cfg) { s.unknown = true return nil, errors.ErrUnknown(KIND_CONFIGTYPE, s.GetType()) + } else { + s.unknown = false } return cfg, nil } diff --git a/pkg/contexts/config/internal/context.go b/pkg/contexts/config/internal/context.go index b34f223221..02777ceafc 100644 --- a/pkg/contexts/config/internal/context.go +++ b/pkg/contexts/config/internal/context.go @@ -46,6 +46,20 @@ type Context interface { ConfigTypes() ConfigTypeScheme + // SkipUnknownConfig can be used to control the behaviour + // for processing unknown configuration object types. + // It returns the previous mode valid before setting the + // new one. + SkipUnknownConfig(bool) bool + + // Validate validates the applied configuration for not using + // unknown configuration types, anymore. This can be used after setting + // SkipUnknownConfig, to check whether there are still unknown types + // which will be skipped. It does not provide information, whether + // config objects were skipped for previous object configuration + // requests. + Validate() error + // GetConfigForData deserialize configuration objects for known // configuration types. GetConfigForData(data []byte, unmarshaler runtime.Unmarshaler) (Config, error) @@ -120,7 +134,8 @@ type coreContext struct { knownConfigTypes ConfigTypeScheme - configs *ConfigStore + configs *ConfigStore + skipUnknownConfig bool } type _context struct { @@ -201,6 +216,12 @@ func (c *_context) ConfigTypes() ConfigTypeScheme { return c.knownConfigTypes } +func (c *_context) SkipUnknownConfig(b bool) bool { + old := c.skipUnknownConfig + c.skipUnknownConfig = b + return old +} + func (c *_context) ConfigForData(data []byte, unmarshaler runtime.Unmarshaler) (Config, error) { return c.knownConfigTypes.Decode(data, unmarshaler) } @@ -217,14 +238,19 @@ func (c *_context) ApplyConfig(spec Config, desc string) error { var unknown error // use temporary view for outbound calls - spec = (&AppliedConfig{config: spec}).eval(newView(c)) - if IsGeneric(spec) { - unknown = errors.ErrUnknown(KIND_CONFIGTYPE, spec.GetType()) + spec, err := (&AppliedConfig{config: spec}).eval(newView(c)) + if err != nil { + if !errors.IsErrUnknownKind(err, KIND_CONFIGTYPE) { + return errors.Wrapf(err, "%s", desc) + } + if !c.skipUnknownConfig { + unknown = err + } + err = nil } c.configs.Apply(spec, desc) - var err error for { // apply directly and also indirectly described configurations if gen, in := c.updater.State(); err != nil || in || gen >= c.configs.Generation() { @@ -236,8 +262,7 @@ func (c *_context) ApplyConfig(spec Config, desc string) error { } } - err = errors.Wrapf(err, "%s", desc) - return err + return errors.Wrapf(err, "%s", desc) } func (c *_context) ApplyData(data []byte, unmarshaler runtime.Unmarshaler, desc string) (Config, error) { @@ -275,7 +300,11 @@ func (c *_context) ApplyTo(gen int64, target interface{}) (int64, error) { list := errors.ErrListf("config apply errors") for _, cfg := range cfgs { - err := errors.Wrapf(cfg.config.ApplyTo(c.WithInfo(cfg.description), target), "%s", cfg.description) + err := cfg.config.ApplyTo(c.WithInfo(cfg.description), target) + if c.skipUnknownConfig && errors.IsErrUnknownKind(err, KIND_CONFIGTYPE) { + err = nil + } + err = errors.Wrapf(err, "%s", cfg.description) if !IsErrNoContext(err) { list.Add(err) } @@ -283,6 +312,17 @@ func (c *_context) ApplyTo(gen int64, target interface{}) (int64, error) { return cur, list.Result() } +func (c *_context) Validate() error { + list := errors.ErrList() + + _, cfgs := c.configs.GetConfigForSelector(c, AllAppliedConfigs) + for _, cfg := range cfgs { + _, err := cfg.eval(newView(c)) + list.Add(err) + } + return list.Result() +} + func (c *_context) AddConfigSet(name string, set *ConfigSet) { c.configs.AddSet(name, set) } diff --git a/pkg/contexts/config/internal/store.go b/pkg/contexts/config/internal/store.go index 0b6bd6e48d..a378edddc3 100644 --- a/pkg/contexts/config/internal/store.go +++ b/pkg/contexts/config/internal/store.go @@ -68,14 +68,15 @@ type AppliedConfig struct { description string } -func (c *AppliedConfig) eval(ctx Context) Config { +func (c *AppliedConfig) eval(ctx Context) (Config, error) { if e, ok := c.config.(Evaluator); ok { n, err := e.Evaluate(ctx) - if err == nil { - c.config = n + if err != nil { + return c.config, err } + c.config = n } - return c.config + return c.config, nil } type ConfigStore struct { diff --git a/pkg/contexts/config/internal/updater.go b/pkg/contexts/config/internal/updater.go index eb372255b6..eca411f54f 100644 --- a/pkg/contexts/config/internal/updater.go +++ b/pkg/contexts/config/internal/updater.go @@ -42,7 +42,7 @@ func TargetFunction[T any](f func() T) func() interface{} { // NewUpdater create a configuration updater for a configuration target // based on a dedicated configuration context. -func NewUpdater(ctx Context, target interface{}) Updater { +func NewUpdater(ctx ContextProvider, target interface{}) Updater { var targetFunc func() interface{} if f, ok := target.(func() interface{}); ok { targetFunc = f @@ -50,14 +50,14 @@ func NewUpdater(ctx Context, target interface{}) Updater { targetFunc = func() interface{} { return target } } return &updater{ - ctx: ctx, + ctx: ctx.ConfigContext(), targetFunc: targetFunc, } } -func NewUpdaterForFactory[T any](ctx Context, t func() T) Updater { +func NewUpdaterForFactory[T any](ctx ContextProvider, t func() T) Updater { return &updater{ - ctx: ctx, + ctx: ctx.ConfigContext(), targetFunc: TargetFunction(t), } } diff --git a/pkg/contexts/config/plugin/type.go b/pkg/contexts/config/plugin/type.go new file mode 100644 index 0000000000..a240644ffc --- /dev/null +++ b/pkg/contexts/config/plugin/type.go @@ -0,0 +1,21 @@ +package plugin + +import ( + "github.com/open-component-model/ocm/pkg/contexts/config/cpi" + "github.com/open-component-model/ocm/pkg/contexts/config/internal" + "github.com/open-component-model/ocm/pkg/runtime" +) + +var _ cpi.Config = (*Config)(nil) + +type Config struct { + runtime.UnstructuredVersionedTypedObject `json:",inline"` +} + +func (c *Config) ApplyTo(context internal.Context, i interface{}) error { + return nil +} + +func New(name string, desc string) cpi.ConfigType { + return cpi.NewConfigType[*Config](name, desc) +} diff --git a/pkg/contexts/credentials/repositories/dockerconfig/default.go b/pkg/contexts/credentials/repositories/dockerconfig/default.go index 54e763d8ec..f29813bbc0 100644 --- a/pkg/contexts/credentials/repositories/dockerconfig/default.go +++ b/pkg/contexts/credentials/repositories/dockerconfig/default.go @@ -3,7 +3,6 @@ package dockerconfig import ( dockercli "github.com/docker/cli/cli/config" "github.com/mandelsoft/filepath/pkg/filepath" - "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" @@ -16,18 +15,15 @@ func init() { defaultconfigregistry.RegisterDefaultConfigHandler(DefaultConfigHandler, desc) } -func DefaultConfigHandler(cfg config.Context) error { +func DefaultConfigHandler(cfg config.Context) (string, config.Config, error) { // use docker config as default config for ocm cli d := filepath.Join(dockercli.Dir(), dockercli.ConfigFileName) if ok, err := vfs.FileExists(osfs.New(), d); ok && err == nil { ccfg := credcfg.New() ccfg.AddRepository(NewRepositorySpec(d, true)) - err = cfg.ApplyConfig(ccfg, d) - if err != nil { - return errors.Wrapf(err, "cannot apply docker config %q", d) - } + return d, ccfg, nil } - return nil + return "", nil, nil } var desc = ` diff --git a/pkg/contexts/credentials/repositories/npm/default.go b/pkg/contexts/credentials/repositories/npm/default.go index aaf6ad3053..1c11e20cd3 100644 --- a/pkg/contexts/credentials/repositories/npm/default.go +++ b/pkg/contexts/credentials/repositories/npm/default.go @@ -5,7 +5,6 @@ import ( "os" "github.com/mandelsoft/filepath/pkg/filepath" - "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" @@ -30,21 +29,18 @@ func DefaultConfig() (string, error) { return filepath.Join(d, ConfigFileName), nil } -func DefaultConfigHandler(cfg config.Context) error { +func DefaultConfigHandler(cfg config.Context) (string, config.Config, error) { // use docker config as default config for ocm cli d, err := DefaultConfig() if err != nil { - return nil + return "", nil, nil } - if ok, err := vfs.FileExists(osfs.New(), d); ok && err == nil { + if ok, err := vfs.FileExists(osfs.OsFs, d); ok && err == nil { ccfg := credcfg.New() ccfg.AddRepository(NewRepositorySpec(d, true)) - err = cfg.ApplyConfig(ccfg, d) - if err != nil { - return errors.Wrapf(err, "cannot apply npm config %q", d) - } + return d, ccfg, nil } - return nil + return "", nil, nil } var desc = fmt.Sprintf(` diff --git a/pkg/contexts/datacontext/attrs/clicfgattr/attr.go b/pkg/contexts/datacontext/attrs/clicfgattr/attr.go new file mode 100644 index 0000000000..f2c05406e2 --- /dev/null +++ b/pkg/contexts/datacontext/attrs/clicfgattr/attr.go @@ -0,0 +1,70 @@ +package clicfgattr + +import ( + "encoding/json" + "fmt" + + "sigs.k8s.io/yaml" + + "github.com/open-component-model/ocm/pkg/contexts/config" + "github.com/open-component-model/ocm/pkg/contexts/datacontext" + "github.com/open-component-model/ocm/pkg/runtime" +) + +const ( + ATTR_KEY = "ocm.software/cliconfig" + ATTR_SHORT = "cliconfig" +) + +func init() { + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) +} + +type AttributeType struct{} + +func (a AttributeType) Name() string { + return ATTR_KEY +} + +func (a AttributeType) Description() string { + return ` +*cliconfigr* Configuration Object passed to command line pluging. +` +} + +func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { + switch c := v.(type) { + case config.Config: + return json.Marshal(v) + case []byte: + if _, err := a.Decode(c, nil); err != nil { + return nil, err + } + return c, nil + default: + return nil, fmt.Errorf("config object required") + } +} + +func (a AttributeType) Decode(data []byte, _ runtime.Unmarshaler) (interface{}, error) { + var c config.GenericConfig + err := yaml.Unmarshal(data, &c) + if err != nil { + return nil, err + } + return &c, nil +} + +//////////////////////////////////////////////////////////////////////////////// + +func Get(ctx datacontext.Context) config.Config { + v := ctx.GetAttributes().GetAttribute(ATTR_KEY) + if v == nil { + return nil + } + return v.(config.Config) +} + +func Set(ctx datacontext.Context, c config.Config) { + ctx.GetAttributes().SetAttribute(ATTR_KEY, c) +} diff --git a/pkg/contexts/datacontext/attrs/logforward/attr.go b/pkg/contexts/datacontext/attrs/logforward/attr.go index 998043e527..3d21519403 100644 --- a/pkg/contexts/datacontext/attrs/logforward/attr.go +++ b/pkg/contexts/datacontext/attrs/logforward/attr.go @@ -17,7 +17,7 @@ const ( ) func init() { - datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}) + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) } type AttributeType struct{} diff --git a/pkg/contexts/datacontext/attrs/rootcertsattr/attr.go b/pkg/contexts/datacontext/attrs/rootcertsattr/attr.go index 21080d59dc..bee68ece4f 100644 --- a/pkg/contexts/datacontext/attrs/rootcertsattr/attr.go +++ b/pkg/contexts/datacontext/attrs/rootcertsattr/attr.go @@ -23,7 +23,7 @@ type ( ) func init() { - datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}) + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) } type AttributeType struct{} diff --git a/pkg/contexts/datacontext/attrs/vfsattr/attr.go b/pkg/contexts/datacontext/attrs/vfsattr/attr.go index 5ba3ef9645..4b93bb7c3a 100644 --- a/pkg/contexts/datacontext/attrs/vfsattr/attr.go +++ b/pkg/contexts/datacontext/attrs/vfsattr/attr.go @@ -17,7 +17,7 @@ const ( ) func init() { - datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}) + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) } type AttributeType struct{} diff --git a/pkg/contexts/datacontext/config/logging/type.go b/pkg/contexts/datacontext/config/logging/type.go index 2b1ef96c8e..f4f13f4880 100644 --- a/pkg/contexts/datacontext/config/logging/type.go +++ b/pkg/contexts/datacontext/config/logging/type.go @@ -4,6 +4,7 @@ import ( "github.com/mandelsoft/logging" logcfg "github.com/mandelsoft/logging/config" + logdata "github.com/open-component-model/ocm/pkg/cobrautils/logopts/logging" "github.com/open-component-model/ocm/pkg/contexts/config/cpi" "github.com/open-component-model/ocm/pkg/contexts/datacontext" local "github.com/open-component-model/ocm/pkg/logging" @@ -30,7 +31,7 @@ type Config struct { ContextType string `json:"contextType,omitempty"` Settings logcfg.Config `json:"settings"` - // ExtraId is used to the context type "default" or "global" to be able + // ExtraId is used for the context type "default", "ocm" or "global" to be able // to reapply the same config again using a different // identity given by the settings hash + the id. ExtraId string `json:"extraId,omitempty"` @@ -67,11 +68,17 @@ func (c *Config) GetType() string { } func (c *Config) ApplyTo(ctx cpi.Context, target interface{}) error { - lctx, ok := target.(logging.ContextProvider) - if !ok { - return cpi.ErrNoContext("logging context") + // first: check for forward configuration + if lc, ok := target.(*logdata.LoggingConfiguration); ok { + switch c.ContextType { + case "default", "ocm", "global", "slave": + lc.LogConfig.DefaultLevel = c.Settings.DefaultLevel + lc.LogConfig.Rules = append(lc.LogConfig.Rules, c.Settings.Rules...) + } + return nil } + // second: main use case is to configure vrious logging contexts switch c.ContextType { // configure local static logging context. // here, config is only applied once for every @@ -79,9 +86,15 @@ func (c *Config) ApplyTo(ctx cpi.Context, target interface{}) error { case "default": return local.Configure(&c.Settings, c.ExtraId) + case "ocm": + return local.ConfigureOCM(&c.Settings, c.ExtraId) + case "global": return local.ConfigureGlobal(&c.Settings, c.ExtraId) + case "slave": + return nil + // configure logging context providers. case "": if _, ok := target.(datacontext.AttributesContext); !ok { @@ -98,6 +111,10 @@ func (c *Config) ApplyTo(ctx cpi.Context, target interface{}) error { return cpi.ErrNoContext(c.ContextType) } } + lctx, ok := target.(logging.ContextProvider) + if !ok { + return cpi.ErrNoContext("logging context") + } return logcfg.DefaultRegistry().Configure(lctx.LoggingContext(), &c.Settings) } diff --git a/pkg/contexts/oci/testhelper/manifests.go b/pkg/contexts/oci/testhelper/manifests.go index c9756bc262..579a577847 100644 --- a/pkg/contexts/oci/testhelper/manifests.go +++ b/pkg/contexts/oci/testhelper/manifests.go @@ -1,7 +1,8 @@ package testhelper import ( - "github.com/open-component-model/ocm/pkg/common" + "github.com/mandelsoft/goutils/testutils" + "github.com/open-component-model/ocm/pkg/contexts/oci" "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/artifactset" @@ -23,7 +24,7 @@ const ( OCILAYER = "manifestlayer" ) -var OCIDigests = common.Properties{ +var OCIDigests = testutils.Substitutions{ "D_OCIMANIFEST1": D_OCIMANIFEST1, "H_OCIARCHMANIFEST1": H_OCIARCHMANIFEST1, "D_OCIMANIFEST2": D_OCIMANIFEST2, diff --git a/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go b/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go index af64f7a942..feffe5ab9b 100644 --- a/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go +++ b/pkg/contexts/ocm/accessmethods/plugin/cmd_test.go @@ -4,6 +4,8 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env" "github.com/mandelsoft/goutils/sliceutils" @@ -13,9 +15,6 @@ import ( "github.com/open-component-model/ocm/pkg/cobrautils/flagsets" "github.com/open-component-model/ocm/pkg/contexts/ocm" "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/options" - "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/plugin/plugins" "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" ) @@ -28,19 +27,19 @@ var _ = Describe("Add with new access method", func() { var env *Environment var ctx ocm.Context var registry plugins.Set + var plugins TempPluginDir BeforeEach(func() { env = NewEnvironment(TestData()) ctx = env.OCMContext() - - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) + plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata")) Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") Expect(p).NotTo(BeNil()) }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/pkg/contexts/ocm/accessmethods/plugin/method_test.go b/pkg/contexts/ocm/accessmethods/plugin/method_test.go index a3af2cf8c6..581c00fbe2 100644 --- a/pkg/contexts/ocm/accessmethods/plugin/method_test.go +++ b/pkg/contexts/ocm/accessmethods/plugin/method_test.go @@ -6,13 +6,12 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env/builder" "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" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" - "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" @@ -30,6 +29,7 @@ var _ = Describe("setup plugin cache", func() { var ctx ocm.Context var registry plugins.Set var env *Builder + var plugins TempPluginDir var accessSpec ocm.AccessSpec @@ -44,14 +44,14 @@ someattr: value env = NewBuilder(nil) ctx = env.OCMContext() - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) + plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata")) Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") Expect(p).NotTo(BeNil()) }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/pkg/contexts/ocm/accessmethods/plugin/testdata/test b/pkg/contexts/ocm/accessmethods/plugin/testdata/test index 71243c2dfc..032ccc5d70 100755 --- a/pkg/contexts/ocm/accessmethods/plugin/testdata/test +++ b/pkg/contexts/ocm/accessmethods/plugin/testdata/test @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -e NAME="$(basename "$0")" diff --git a/pkg/contexts/ocm/actionhandler/plugin/action_test.go b/pkg/contexts/ocm/actionhandler/plugin/action_test.go index 2881227ee7..f684674d38 100644 --- a/pkg/contexts/ocm/actionhandler/plugin/action_test.go +++ b/pkg/contexts/ocm/actionhandler/plugin/action_test.go @@ -6,14 +6,13 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env/builder" "github.com/open-component-model/ocm/pkg/contexts/datacontext/action/handlers" oci_repository_prepare "github.com/open-component-model/ocm/pkg/contexts/oci/actions/oci-repository-prepare" "github.com/open-component-model/ocm/pkg/contexts/ocm" "github.com/open-component-model/ocm/pkg/contexts/ocm/actionhandler/plugin" - "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/plugin/plugins" "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" ) @@ -24,17 +23,18 @@ var _ = Describe("plugin action handler", func() { var ctx ocm.Context var registry plugins.Set var env *Builder + var plugins TempPluginDir BeforeEach(func() { env = NewBuilder(nil) ctx = env.OCMContext() - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) + plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata")) p := registry.Get("action") Expect(p).NotTo(BeNil()) }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/maven/blobhandler_test.go b/pkg/contexts/ocm/blobhandler/handlers/generic/maven/blobhandler_test.go index 66e6eb4d69..a0b6b03914 100644 --- a/pkg/contexts/ocm/blobhandler/handlers/generic/maven/blobhandler_test.go +++ b/pkg/contexts/ocm/blobhandler/handlers/generic/maven/blobhandler_test.go @@ -32,6 +32,10 @@ var _ = Describe("blobhandler generic maven tests", func() { repo = maven.NewFileRepository(MAVEN_PATH, env.FileSystem()) }) + AfterEach(func() { + env.Cleanup() + }) + It("Unmarshal upload response Body", func() { resp := `{ "repo" : "ocm-mvn-test", "path" : "/open-component-model/hello-ocm/0.0.2/hello-ocm-0.0.2.jar", diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/plugin/upload_test.go b/pkg/contexts/ocm/blobhandler/handlers/generic/plugin/upload_test.go index 1a288c9036..a356175061 100644 --- a/pkg/contexts/ocm/blobhandler/handlers/generic/plugin/upload_test.go +++ b/pkg/contexts/ocm/blobhandler/handlers/generic/plugin/upload_test.go @@ -10,6 +10,7 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env/builder" "github.com/mandelsoft/filepath/pkg/filepath" @@ -19,8 +20,6 @@ import ( "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" - "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" "github.com/open-component-model/ocm/pkg/contexts/ocm/blobhandler/handlers/generic/plugin" metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" @@ -87,6 +86,7 @@ var _ = Describe("setup plugin cache", func() { var registry plugins.Set var repodir string var env *Builder + var plugins TempPluginDir accessSpec := NewAccessSpec(MEDIA, "given", REPO) repoSpec := NewRepoSpec(REPO) @@ -96,8 +96,7 @@ var _ = Describe("setup plugin cache", func() { env = NewBuilder(nil) ctx = env.OCMContext() - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) + plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata")) p := registry.Get("test") Expect(p).NotTo(BeNil()) @@ -119,6 +118,7 @@ var _ = Describe("setup plugin cache", func() { }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() os.RemoveAll(repodir) }) @@ -131,7 +131,8 @@ var _ = Describe("setup plugin cache", func() { defer Close(cv, "source version") _, _, err := plugin.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", []byte("{}")) - MustFailWithMessage(err, "plugin uploader test/testuploader: path missing in repository spec") + fmt.Printf("error %q\n", err) + MustFailWithMessage(err, "plugin uploader test/testuploader: error processing plugin command upload: path missing in repository spec") repospec := Must(json.Marshal(repoSpec)) name, keys, err := plugin.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", repospec) MustBeSuccessful(err) @@ -169,7 +170,8 @@ var _ = Describe("setup plugin cache", func() { cv := Must(repo.LookupComponentVersion(COMP, VERS)) defer Close(cv, "source version") - MustFailWithMessage(blobhandler.RegisterHandlerByName(ctx, "plugin/test", []byte("{}"), blobhandler.ForArtifactType(RSCTYPE)), "plugin uploader test/testuploader: path missing in repository spec") + MustFailWithMessage(blobhandler.RegisterHandlerByName(ctx, "plugin/test", []byte("{}"), blobhandler.ForArtifactType(RSCTYPE)), + "plugin uploader test/testuploader: error processing plugin command upload: path missing in repository spec") repospec := Must(json.Marshal(repoSpec)) MustBeSuccessful(blobhandler.RegisterHandlerByName(ctx, "plugin/test", repospec)) diff --git a/pkg/contexts/ocm/download/handlers/plugin/download_test.go b/pkg/contexts/ocm/download/handlers/plugin/download_test.go index 75fccb7737..79f46a7f2d 100644 --- a/pkg/contexts/ocm/download/handlers/plugin/download_test.go +++ b/pkg/contexts/ocm/download/handlers/plugin/download_test.go @@ -9,6 +9,7 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env/builder" "github.com/mandelsoft/vfs/pkg/vfs" @@ -17,8 +18,6 @@ import ( "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" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" - "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" @@ -68,14 +67,14 @@ var _ = Describe("setup plugin cache", func() { var registry plugins.Set var repodir string var env *Builder + var plugins TempPluginDir BeforeEach(func() { repodir = Must(os.MkdirTemp(os.TempDir(), "uploadtest-*")) env = NewBuilder(nil) ctx = env.OCMContext() - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) + plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata")) p := registry.Get("test") Expect(p).NotTo(BeNil()) @@ -83,6 +82,7 @@ var _ = Describe("setup plugin cache", func() { }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() os.RemoveAll(repodir) }) diff --git a/pkg/contexts/ocm/labels/routingslip/types/plugin/cmd_test.go b/pkg/contexts/ocm/labels/routingslip/types/plugin/cmd_test.go index 984e2b8e35..5d416ea220 100644 --- a/pkg/contexts/ocm/labels/routingslip/types/plugin/cmd_test.go +++ b/pkg/contexts/ocm/labels/routingslip/types/plugin/cmd_test.go @@ -4,6 +4,7 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env" "github.com/mandelsoft/goutils/sliceutils" @@ -13,7 +14,6 @@ import ( "github.com/open-component-model/ocm/pkg/cobrautils/flagsets" "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/options" "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/labels/routingslip" "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" ) @@ -27,12 +27,13 @@ const ( var _ = Describe("Test Environment", func() { var env *Environment + var plugins TempPluginDir BeforeEach(func() { env = NewEnvironment(TestData()) ctx := env.OCMContext() - plugindirattr.Set(ctx, "testdata") + plugins = Must(ConfigureTestPlugins(env, "testdata")) registry := plugincacheattr.Get(ctx) Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") @@ -40,6 +41,7 @@ var _ = Describe("Test Environment", func() { }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/pkg/contexts/ocm/labels/routingslip/types/plugin/entry_test.go b/pkg/contexts/ocm/labels/routingslip/types/plugin/entry_test.go index 794c0b6a9d..9f98c94434 100644 --- a/pkg/contexts/ocm/labels/routingslip/types/plugin/entry_test.go +++ b/pkg/contexts/ocm/labels/routingslip/types/plugin/entry_test.go @@ -6,14 +6,13 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env" "github.com/spf13/pflag" "github.com/open-component-model/ocm/pkg/cobrautils/flagsets" "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/labels/routingslip/spi" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" @@ -23,18 +22,19 @@ var _ = Describe("setup plugin cache", func() { var ctx ocm.Context var registry plugins.Set var env *Environment + var plugins TempPluginDir BeforeEach(func() { env = NewEnvironment() ctx = env.OCMContext() - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) + plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata")) Expect(registration.RegisterExtensions(ctx)).To(Succeed()) p := registry.Get("test") Expect(p).NotTo(BeNil()) }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/pkg/contexts/ocm/plugin/cache/plugin.go b/pkg/contexts/ocm/plugin/cache/plugin.go index 4547e91614..d38ec85db8 100644 --- a/pkg/contexts/ocm/plugin/cache/plugin.go +++ b/pkg/contexts/ocm/plugin/cache/plugin.go @@ -11,7 +11,7 @@ type Plugin = *pluginImpl // //nolint: errname // is no error. type pluginImpl struct { name string - source *PluginSource + source *PluginInstallationInfo descriptor *descriptor.Descriptor path string error string @@ -36,7 +36,7 @@ func NewPlugin(name string, path string, desc *descriptor.Descriptor, errmsg str return p } -func (p *pluginImpl) GetSource() *PluginSource { +func (p *pluginImpl) GetInstallationInfo() *PluginInstallationInfo { return p.source } diff --git a/pkg/contexts/ocm/plugin/cache/plugindir.go b/pkg/contexts/ocm/plugin/cache/plugindir.go index 374bf4ceac..6f669275b5 100644 --- a/pkg/contexts/ocm/plugin/cache/plugindir.go +++ b/pkg/contexts/ocm/plugin/cache/plugindir.go @@ -7,12 +7,14 @@ import ( "github.com/mandelsoft/filepath/pkg/filepath" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/finalizer" "github.com/mandelsoft/goutils/maputils" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/descriptor" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/info" + "github.com/open-component-model/ocm/pkg/filelock" ) type PluginDir = *pluginDirImpl @@ -54,7 +56,7 @@ func (c *pluginDirImpl) Get(name string) Plugin { func (c *pluginDirImpl) add(name string, desc *descriptor.Descriptor, path string, errmsg string, list *errors.ErrorList) { c.plugins[name] = NewPlugin(name, path, desc, errmsg) if path != "" { - src, err := ReadPluginSource(filepath.Dir(path), filepath.Base(path)) + src, err := readPluginInstalltionInfo(filepath.Dir(path), filepath.Base(path)) if err != nil && list != nil { list.Add(fmt.Errorf("%s: %s", name, err.Error())) return @@ -68,8 +70,31 @@ func (c *pluginDirImpl) add(name string, desc *descriptor.Descriptor, path strin } func (c *pluginDirImpl) scan(path string) error { + fs := osfs.OsFs + + ok, err := vfs.Exists(fs, path) + if err != nil { + return err + } + if !ok { + return nil + } + ok, err = vfs.IsDir(fs, path) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("plugin path %q is no directory", path) + } + lockfile, err := filelock.MutexFor(path) + if err != nil { + return err + } + + var finalize finalizer.Finalizer + defer finalize.Finalize() + DirectoryCache.numOfScans++ - fs := osfs.New() entries, err := vfs.ReadDir(fs, path) if err != nil { return err @@ -77,23 +102,62 @@ func (c *pluginDirImpl) scan(path string) error { list := errors.ErrListf("scanning %q", path) for _, fi := range entries { if fi.Mode()&0o001 != 0 { - execpath := filepath.Join(path, fi.Name()) - desc, err := GetPluginInfo(execpath) + loop := finalize.Nested() + lock, err := lockfile.Lock() if err != nil { - c.add(fi.Name(), nil, execpath, err.Error(), list) - continue + return err } + loop.Close(lock) + + execpath := filepath.Join(path, fi.Name()) + desc, err := getCachedPluginInfo(path, fi.Name()) - if desc.PluginName != fi.Name() { - c.add(fi.Name(), nil, execpath, fmt.Sprintf("nmatching plugin name %q", desc.PluginName), list) - continue + errmsg := "" + + if err != nil { + errmsg = err.Error() + } else { + if desc.PluginName != fi.Name() { + errmsg = fmt.Sprintf("nmatching plugin name %q", desc.PluginName) + } } - c.add(desc.PluginName, desc, execpath, "", nil) + c.add(fi.Name(), desc, execpath, errmsg, list) + loop.Finalize() } } return list.Result() } +func GetCachedPluginInfo(dir string, name string) (*descriptor.Descriptor, error) { + l, err := filelock.LockDir(dir) + if err != nil { + return nil, err + } + defer l.Close() + return getCachedPluginInfo(dir, name) +} + +func getCachedPluginInfo(dir string, name string) (*descriptor.Descriptor, error) { + src, err := readPluginInstalltionInfo(dir, name) + if err != nil { + return nil, err + } + execpath := filepath.Join(dir, name) + if !src.IsValidPluginInfo(execpath) { + mod, err := src.UpdatePluginInfo(filepath.Join(dir, name)) + if err != nil { + return nil, err + } + if mod { + err := writePluginInstallationInfo(src, dir, name) + if err != nil { + return nil, err + } + } + } + return src.PluginInfo.Descriptor, nil +} + func GetPluginInfo(execpath string) (*descriptor.Descriptor, error) { result, err := Exec(execpath, nil, nil, nil, info.NAME) if err != nil { diff --git a/pkg/contexts/ocm/plugin/cache/updater.go b/pkg/contexts/ocm/plugin/cache/updater.go index cde2925a3a..cd40f57981 100644 --- a/pkg/contexts/ocm/plugin/cache/updater.go +++ b/pkg/contexts/ocm/plugin/cache/updater.go @@ -5,7 +5,9 @@ import ( "fmt" "io" "os" + "reflect" "runtime" + "time" "github.com/Masterminds/semver/v3" "github.com/mandelsoft/filepath/pkg/filepath" @@ -20,15 +22,73 @@ import ( "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/extraid" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/descriptor" + "github.com/open-component-model/ocm/pkg/filelock" "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 PluginInfo struct { + Size int64 `json:"size,omitempty"` + ModTime time.Time `json:"modtime,omitempty"` + Descriptor *descriptor.Descriptor `json:"descriptor,omitempty"` +} + +type PluginSourceInfo struct { + Repository *cpi.GenericRepositorySpec `json:"repository,omitempty"` + Component string `json:"component,omitempty"` + Version string `json:"version,omitempty"` + Resource string `json:"resource,omitempty"` +} + +type PluginInstallationInfo struct { + PluginSourceInfo `json:",inline"` + PluginInfo *PluginInfo `json:"info,omitempty"` +} + +func (p *PluginInstallationInfo) GetInstallationSourceDescription() string { + if p != nil && p.HasSourceInfo() { + return p.Component + ":" + p.Version + } + return "local" +} + +func (p *PluginInstallationInfo) HasSourceInfo() bool { + return p.Repository != nil && p.Component != "" && p.Version != "" && p.Resource != "" +} + +func (p *PluginInstallationInfo) IsValidPluginInfo(execpath string) bool { + if !p.HasPluginInfo() { + return false + } + fi, err := os.Stat(execpath) + if err != nil { + return false + } + return fi.Size() == p.PluginInfo.Size && fi.ModTime() == p.PluginInfo.ModTime +} + +func (p *PluginInstallationInfo) UpdatePluginInfo(execpath string) (bool, error) { + desc, err := GetPluginInfo(execpath) + if err != nil { + return false, err + } + fi, err := os.Stat(execpath) + if err != nil { + return false, err + } + n := &PluginInfo{ + Size: fi.Size(), + ModTime: fi.ModTime(), + Descriptor: desc, + } + mod := !reflect.DeepEqual(n, p.PluginInfo) + p.PluginInfo = n + return mod, nil +} + +func (p *PluginInstallationInfo) HasPluginInfo() bool { + return p.PluginInfo != nil } type PluginUpdater struct { @@ -50,19 +110,6 @@ func NewPluginUpdater(ctx ocm.ContextProvider, printer common.Printer) *PluginUp } } -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 == "" { @@ -87,10 +134,13 @@ func (o *PluginUpdater) Update(session ocm.Session, name string) error { if dir == "" { return fmt.Errorf("no plugin dir configured") } - src, err := ReadPluginSource(dir, name) + src, err := readPluginInstalltionInfo(dir, name) if err != nil { return err } + if !src.HasSourceInfo() { + return fmt.Errorf("no source information available for plugin %s", name) + } o.Current = src.Version repo, err := session.LookupRepository(o.Context, src.Repository) if err != nil { @@ -236,10 +286,6 @@ func (o *PluginUpdater) download(session ocm.Session, cv ocm.ComponentVersionAcc } 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 { @@ -248,6 +294,12 @@ func (o *PluginUpdater) download(session ocm.Session, cv ocm.ComponentVersionAcc } dir := plugindirattr.Get(o.Context) if dir != "" { + lock, err := filelock.LockDir(dir) + if err != nil { + return err + } + defer lock.Close() + target := filepath.Join(dir, desc.PluginName) verb := "installing" @@ -273,7 +325,7 @@ func (o *PluginUpdater) download(session ocm.Session, cv ocm.ComponentVersionAcc _, 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)) + utils2.IgnoreError(SetPluginSourceInfo(dir, cv, found.Meta().Name, desc.PluginName)) if err != nil { return errors.Wrapf(err, "cannot copy plugin file %s", target) } @@ -293,19 +345,30 @@ 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 { +func SetPluginSourceInfo(dir string, cv ocm.ComponentVersionAccess, rsc, name string) error { + src, err := readPluginInstalltionInfo(dir, name) + if err != nil { + return err + } spec, err := cpi.ToGenericRepositorySpec(cv.Repository().GetSpecification()) if err != nil { return err } - cv.Repository().GetSpecification() - src := &PluginSource{ + src.PluginSourceInfo = PluginSourceInfo{ Repository: spec, Component: cv.GetName(), Version: cv.GetVersion(), Resource: rsc, } + _, err = src.UpdatePluginInfo(filepath.Join(dir, name)) + if err != nil { + return err + } + return writePluginInstallationInfo(src, dir, name) +} + +func writePluginInstallationInfo(src *PluginInstallationInfo, dir string, name string) error { data, err := json.Marshal(src) if err != nil { return err @@ -314,13 +377,16 @@ func WritePluginSource(dir string, cv ocm.ComponentVersionAccess, rsc, name stri return os.WriteFile(filepath.Join(dir, "."+name+".info"), data, 0o644) } -func ReadPluginSource(dir string, name string) (*PluginSource, error) { +func readPluginInstalltionInfo(dir string, name string) (*PluginInstallationInfo, 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) + if !errors.Is(err, os.ErrNotExist) { + return nil, errors.Wrapf(err, "cannot read plugin info for %s", name) + } + return &PluginInstallationInfo{}, nil } - var src PluginSource + var src PluginInstallationInfo if err := json.Unmarshal(data, &src); err != nil { return nil, errors.Wrapf(err, "cannot unmarshal source information") } diff --git a/pkg/contexts/ocm/plugin/common/describe.go b/pkg/contexts/ocm/plugin/common/describe.go index c7cb53490d..c39b217453 100644 --- a/pkg/contexts/ocm/plugin/common/describe.go +++ b/pkg/contexts/ocm/plugin/common/describe.go @@ -70,6 +70,16 @@ func DescribePluginDescriptorCapabilities(reg api.ActionTypeRegistry, d *descrip out.Printf("Label Merge Specifications:\n") DescribeLabelMergeSpecifications(d, out) } + if len(d.Commands) > 0 { + out.Printf("\n") + out.Printf("CLI Extensions:\n") + DescribeCLIExtensions(d, out) + } + if len(d.ConfigTypes) > 0 { + out.Printf("\n") + out.Printf("Config Types for CLI Command Extensions:\n") + DescribeConfigTypes(d, out) + } } type MethodInfo struct { @@ -389,6 +399,124 @@ func DescribeValueSets(d *descriptor.Descriptor, out common.Printer) { } } +func DescribeCLIExtensions(d *descriptor.Descriptor, out common.Printer) { + handlers := map[string]descriptor.CommandDescriptor{} + for _, h := range d.Commands { + handlers[h.GetName()] = h + } + + for _, n := range utils2.StringMapKeys(handlers) { + a := handlers[n] + s := a.Short + if s != "" { + s = " (" + s + ")" + } + out.Printf("- Name: %s%s\n", n, s) + if a.Description != "" { + if len(a.ObjectType) > 0 { + out.Printf(" Object: %s\n", a.ObjectType) + } + if len(a.Verb) > 0 { + out.Printf(" Verb: %s\n", a.Verb) + } + if len(a.Realm) > 0 { + out.Printf(" Realm: %s\n", a.Realm) + } + if len(a.Usage) > 0 { + usage := "" + if a.Verb != "" { + usage += " " + a.Verb + if a.ObjectType != "" { + usage += " " + a.ObjectType + } else { + usage += " " + a.Name + } + } else { + usage += " " + a.Name + } + i := strings.Index(a.Usage, " ") + if i > 0 { + usage += a.Usage[i:] + } + out.Printf(" Usage: %s\n", usage[1:]) + } + if a.Description != "" { + out.Printf("%s\n", utils2.IndentLines(a.Description, " ")) + } + if a.Example != "" { + out.Printf(" Example:\n") + out.Printf("%s\n", utils2.IndentLines(a.Example, " ")) + } + } + } +} + +type TypeInfo struct { + Name string + Description string + Versions map[string]*TypeVersion +} + +type TypeVersion struct { + Name string + Format string +} + +func GetTypeInfo(types []descriptor.ConfigTypeDescriptor) map[string]*TypeInfo { + found := map[string]*TypeInfo{} + for _, m := range types { + i := found[m.Name] + if i == nil { + i = &TypeInfo{ + Name: m.Name, + Description: m.Description, + Versions: map[string]*TypeVersion{}, + } + 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 = &TypeVersion{ + Name: vers, + } + i.Versions[vers] = v + } + if v.Format == "" { + v.Format = m.Format + } + } + return found +} + +func DescribeConfigTypes(d *descriptor.Descriptor, out common.Printer) { + types := GetTypeInfo(d.ConfigTypes) + + for _, n := range utils2.StringMapKeys(types) { + out.Printf("- Name: %s\n", n) + m := types[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) + } + } + } +} + type Describable interface { Describe() string } diff --git a/pkg/contexts/ocm/plugin/descriptor/descriptor.go b/pkg/contexts/ocm/plugin/descriptor/descriptor.go index a0c9d01bcf..bae1499f23 100644 --- a/pkg/contexts/ocm/plugin/descriptor/descriptor.go +++ b/pkg/contexts/ocm/plugin/descriptor/descriptor.go @@ -9,11 +9,12 @@ import ( const VERSION = "v1" type Descriptor struct { - Version string `json:"version,omitempty"` - PluginName string `json:"pluginName"` - PluginVersion string `json:"pluginVersion"` - Short string `json:"shortDescription"` - Long string `json:"description"` + Version string `json:"version,omitempty"` + PluginName string `json:"pluginName"` + PluginVersion string `json:"pluginVersion"` + Short string `json:"shortDescription"` + Long string `json:"description"` + ForwardLogging bool `json:"forwardLogging"` Actions []ActionDescriptor `json:"actions,omitempty"` AccessMethods []AccessMethodDescriptor `json:"accessMethods,omitempty"` @@ -22,6 +23,8 @@ type Descriptor struct { ValueMergeHandlers List[ValueMergeHandlerDescriptor] `json:"valueMergeHandlers,omitempty"` LabelMergeSpecifications List[LabelMergeSpecification] `json:"labelMergeSpecifications,omitempty"` ValueSets List[ValueSetDescriptor] `json:"valuesets,omitempty"` + Commands List[CommandDescriptor] `json:"commands,omitempty"` + ConfigTypes List[ConfigTypeDescriptor] `json:"configTypes,omitempty"` } //////////////////////////////////////////////////////////////////////////////// @@ -49,6 +52,12 @@ func (d *Descriptor) Capabilities() []string { if len(d.LabelMergeSpecifications) > 0 { caps = append(caps, "Label Merge Specs") } + if len(d.Commands) > 0 { + caps = append(caps, "CLI Commands") + } + if len(d.ConfigTypes) > 0 { + caps = append(caps, "Config Types") + } return caps } @@ -115,6 +124,21 @@ type AccessMethodDescriptor struct { //////////////////////////////////////////////////////////////////////////////// +type ValueTypeDefinition struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` + Description string `json:"description"` + Format string `json:"format"` +} + +func (d ValueTypeDefinition) GetName() string { + return d.Name +} + +func (d ValueTypeDefinition) GetDescription() string { + return d.Description +} + type ValueSetDescriptor struct { ValueSetDefinition `json:",inline"` Purposes []string `json:"purposes"` @@ -123,19 +147,8 @@ type ValueSetDescriptor struct { const PURPOSE_ROUTINGSLIP = "routingslip" type ValueSetDefinition struct { - Name string `json:"name"` - Version string `json:"version,omitempty"` - Description string `json:"description"` - Format string `json:"format"` - CLIOptions []CLIOption `json:"options,omitempty"` -} - -func (d ValueSetDefinition) GetName() string { - return d.Name -} - -func (d ValueSetDefinition) GetDescription() string { - return d.Description + ValueTypeDefinition + CLIOptions []CLIOption `json:"options,omitempty"` } //////////////////////////////////////////////////////////////////////////////// @@ -201,6 +214,32 @@ func (a ActionDescriptor) GetDescription() string { //////////////////////////////////////////////////////////////////////////////// +type CommandDescriptor struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + ObjectType string `json:"objectName,omitempty"` + Usage string `json:"usage,omitempty"` + Short string `json:"short,omitempty"` + Example string `json:"example,omitempty"` + Realm string `json:"realm,omitempty"` + Verb string `json:"verb,omitempty"` + CLIConfigRequired bool `json:"cliconfig,omitempty"` +} + +func (a CommandDescriptor) GetName() string { + return a.Name +} + +func (a CommandDescriptor) GetDescription() string { + return a.Description +} + +//////////////////////////////////////////////////////////////////////////////// + +type ConfigTypeDescriptor = ValueTypeDefinition + +//////////////////////////////////////////////////////////////////////////////// + type CLIOption struct { Name string `json:"name"` Type string `json:"type,omitempty"` diff --git a/pkg/contexts/ocm/plugin/interface.go b/pkg/contexts/ocm/plugin/interface.go index 2745249816..fcb166bfd6 100644 --- a/pkg/contexts/ocm/plugin/interface.go +++ b/pkg/contexts/ocm/plugin/interface.go @@ -26,6 +26,7 @@ type ( UploaderKeySet = descriptor.UploaderKeySet ValueSetDefinition = descriptor.ValueSetDefinition ValueSetDescriptor = descriptor.ValueSetDescriptor + CommandDescriptor = descriptor.CommandDescriptor AccessSpecInfo = internal.AccessSpecInfo UploadTargetSpecInfo = internal.UploadTargetSpecInfo diff --git a/pkg/contexts/ocm/plugin/plugin.go b/pkg/contexts/ocm/plugin/plugin.go index f90f693ec0..062510787c 100644 --- a/pkg/contexts/ocm/plugin/plugin.go +++ b/pkg/contexts/ocm/plugin/plugin.go @@ -5,15 +5,20 @@ import ( "encoding/json" "fmt" "io" + "os" "strings" "sync" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/finalizer" + "github.com/mandelsoft/vfs/pkg/vfs" "github.com/open-component-model/ocm/pkg/cobrautils/flagsets" + "github.com/open-component-model/ocm/pkg/cobrautils/logopts/logging" "github.com/open-component-model/ocm/pkg/contexts/credentials" "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi" "github.com/open-component-model/ocm/pkg/contexts/credentials/identity/hostpath" + "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/clicfgattr" "github.com/open-component-model/ocm/pkg/contexts/ocm" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" @@ -24,6 +29,7 @@ import ( accval "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod/validate" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/action" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/action/execute" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/command" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/download" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/mergehandler" merge "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/mergehandler/execute" @@ -76,6 +82,75 @@ func (p *pluginImpl) SetConfig(config json.RawMessage) { p.config = config } +func (p *pluginImpl) Exec(r io.Reader, w io.Writer, args ...string) (result []byte, rerr error) { + var ( + finalize finalizer.Finalizer + err error + logfile *os.File + ) + + defer finalize.FinalizeWithErrorPropagationf(&rerr, "error processing plugin command %s", args[0]) + + if p.GetDescriptor().ForwardLogging { + logfile, err = os.CreateTemp("", "ocm-plugin-log-*") + if rerr != nil { + return nil, err + } + logfile.Close() + finalize.With(func() error { + return os.Remove(logfile.Name()) + }, "failed to remove temporary log file %s", logfile.Name()) + + lcfg := &logging.LoggingConfiguration{} + _, err = p.Context().ConfigContext().ApplyTo(0, lcfg) + if err != nil { + return nil, errors.Wrapf(err, "cannot extract plugin logging configration") + } + lcfg.LogFileName = logfile.Name() + data, err := json.Marshal(lcfg) + if err != nil { + return nil, errors.Wrapf(err, "cannot marshal plugin logging configration") + } + args = append([]string{"--" + ppi.OptPlugingLogConfig, string(data)}, args...) + } + + if len(p.config) == 0 { + p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args) + } else { + p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args, "config", p.config) + } + data, err := cache.Exec(p.Path(), p.config, r, w, args...) + + if logfile != nil { + r, oerr := os.OpenFile(logfile.Name(), vfs.O_RDONLY, 0o600) + if oerr == nil { + finalize.Close(r, "plugin logfile", logfile.Name()) + w := p.ctx.LoggingContext().Tree().LogWriter() + if w == nil { + if logging.GlobalLogFile != nil { + w = logging.GlobalLogFile.File() + } + if w == nil { + w = os.Stderr + } + } + + // weaken the sync problem when merging log files. + // If a SyncWriter is used, the copy is done under a write lock. + // This is only a solution, if the log records are written + // by single write calls. + // The underlying logging apis do not expose their + // sync mechanism for writing log records. + if writer, ok := w.(io.ReaderFrom); ok { + writer.ReadFrom(r) + } else { + io.Copy(w, r) + } + } + } + return data, err +} + func (p *pluginImpl) MergeValue(specification *valuemergehandler.Specification, local, inbound valuemergehandler.Value) (bool, *valuemergehandler.Value, error) { desc := p.GetValueMappingDescriptor(specification.Algorithm) if desc == nil { @@ -293,15 +368,6 @@ func (p *pluginImpl) Download(name string, r io.Reader, artType, mimeType, targe return m.Path != "", m.Path, nil } -func (p *pluginImpl) Exec(r io.Reader, w io.Writer, args ...string) ([]byte, error) { - if len(p.config) == 0 { - p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args) - } else { - p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args, "config", p.config) - } - return cache.Exec(p.Path(), p.config, r, w, args...) -} - func (p *pluginImpl) ValidateValueSet(purpose string, spec []byte) (*ppi.ValueSetInfo, error) { result, err := p.Exec(nil, nil, valueset.Name, vsval.Name, purpose, string(spec)) if err != nil { @@ -347,3 +413,48 @@ func (p *pluginImpl) ComposeValueSet(purpose, name string, opts flagsets.ConfigO } return nil } + +func (p *pluginImpl) Command(name string, reader io.Reader, writer io.Writer, cmdargs []string) (rerr error) { + var finalize finalizer.Finalizer + cmd := p.GetDescriptor().Commands.Get(name) + if cmd == nil { + return errors.ErrNotFound("command", name) + } + + defer finalize.FinalizeWithErrorPropagationf(&rerr, "error processing plugin command call %s", name) + + var f vfs.File + + args := []string{command.Name} + + a := clicfgattr.Get(p.Context()) + if a != nil && cmd.CLIConfigRequired { + cfgdata, err := json.Marshal(a) + if err != nil { + return errors.Wrapf(err, "cannot marshal CLI config") + } + // cannot use a vfs here, since it's not possible to pass it to the plugin + f, err = os.CreateTemp("", "cli-om-config-*") + if err != nil { + return err + } + finalize.With(func() error { + return os.Remove(f.Name()) + }, "failed to remove temporary config file %s", f.Name()) + + _, err = f.Write(cfgdata) + if err != nil { + f.Close() + return err + } + err = f.Close() + if err != nil { + return err + } + args = append(args, "--"+command.OptCliConfig, f.Name(), name) + } + args = append(append(args, name), cmdargs...) + + _, err := p.Exec(reader, writer, args...) + return err +} diff --git a/pkg/contexts/ocm/plugin/plugin_test.go b/pkg/contexts/ocm/plugin/plugin_test.go index babefc5d8d..d0499c1394 100644 --- a/pkg/contexts/ocm/plugin/plugin_test.go +++ b/pkg/contexts/ocm/plugin/plugin_test.go @@ -6,6 +6,7 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" common2 "github.com/open-component-model/ocm/pkg/common" "github.com/open-component-model/ocm/pkg/contexts/credentials" @@ -27,12 +28,16 @@ import ( var _ = Describe("setup plugin cache", func() { var ctx ocm.Context var registry plugins.Set + var plugins TempPluginDir BeforeEach(func() { cache.DirectoryCache.Reset() ctx = ocm.New() - plugindirattr.Set(ctx, "testdata") - registry = plugincacheattr.Get(ctx) + plugins, registry = Must2(ConfigureTestPlugins2(ctx, "testdata")) + }) + + AfterEach(func() { + plugins.Cleanup() }) It("finds plugin", func() { @@ -52,7 +57,7 @@ var _ = Describe("setup plugin cache", func() { It("scans only once", func() { ctx = ocm.New() - plugindirattr.Set(ctx, "testdata") + plugindirattr.Set(ctx, plugins.Path()) registry = plugincacheattr.Get(ctx) p := registry.Get("test") diff --git a/pkg/contexts/ocm/plugin/ppi/clicmd/options.go b/pkg/contexts/ocm/plugin/ppi/clicmd/options.go new file mode 100644 index 0000000000..d8353d7386 --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/clicmd/options.go @@ -0,0 +1,75 @@ +package clicmd + +import ( + "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/goutils/optionutils" +) + +type Options struct { + RequireCLIConfig *bool + Verb string + ObjectType string + Realm string +} + +type Option = optionutils.Option[*Options] + +//////////////////////////////////////////////////////////////////////////////// + +func (o *Options) ApplyTo(opts *Options) { + if opts == nil { + return + } + optionutils.Transfer(&opts.Verb, o.Verb) + optionutils.Transfer(&opts.ObjectType, o.ObjectType) + optionutils.Transfer(&opts.Realm, o.Realm) + optionutils.Transfer(&opts.RequireCLIConfig, o.RequireCLIConfig) +} + +//////////////////////////////////////////////////////////////////////////////// + +type verb string + +func (o verb) ApplyTo(opts *Options) { + opts.Verb = string(o) +} + +func WithVerb(v string) Option { + return verb(v) +} + +//////////////////////////////////////////////////////////////////////////////// + +type objtype string + +func (o objtype) ApplyTo(opts *Options) { + opts.ObjectType = string(o) +} + +func WithObjectType(r string) Option { + return objtype(r) +} + +//////////////////////////////////////////////////////////////////////////////// + +type realm string + +func (o realm) ApplyTo(opts *Options) { + opts.Realm = string(o) +} + +func WithRealm(r string) Option { + return realm(r) +} + +//////////////////////////////////////////////////////////////////////////////// + +type cliconfig bool + +func (o cliconfig) ApplyTo(opts *Options) { + opts.RequireCLIConfig = optionutils.BoolP(o) +} + +func WithCLIConfig(r ...bool) Option { + return cliconfig(general.OptionalDefaultedBool(true, r...)) +} diff --git a/pkg/contexts/ocm/plugin/ppi/clicmd/utils.go b/pkg/contexts/ocm/plugin/ppi/clicmd/utils.go new file mode 100644 index 0000000000..b70369c90d --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/clicmd/utils.go @@ -0,0 +1,85 @@ +package clicmd + +import ( + _ "github.com/open-component-model/ocm/cmds/ocm/clippi/config" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/optionutils" + "github.com/spf13/cobra" + + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" +) + +//////////////////////////////////////////////////////////////////////////////// + +type CobraCommand struct { + cmd *cobra.Command + verb string + realm string + objname string + cliConfigRequired bool +} + +var _ ppi.Command = (*CobraCommand)(nil) + +// NewCLICommand created a CLI command based on a preconfigured cobra.Command. +// Optionally, a verb can be specified. If given additionally a realm +// can be given. +// verb and realm are used to add the command at the appropriate places in +// the command hierarchy of the ocm CLI. +// If nothing is specified, the command will be a new top-level command. +// To access the configured ocm context use the Context attribute +// of the cobra command. The ocm context is bound to it. +// +// ocm.FromContext(cmd.Context()) +func NewCLICommand(cmd *cobra.Command, opts ...Option) (ppi.Command, error) { + eff := optionutils.EvalOptions(opts...) + if eff.Verb == "" && eff.Realm != "" { + return nil, errors.New("realm without verb not allowed") + } + cmd.DisableFlagsInUseLine = true + return &CobraCommand{cmd, eff.Verb, eff.Realm, eff.ObjectType, optionutils.AsBool(eff.RequireCLIConfig, false)}, nil +} + +func (c *CobraCommand) Name() string { + return c.cmd.Name() +} + +func (c *CobraCommand) Description() string { + return c.cmd.Long +} + +func (c *CobraCommand) Usage() string { + return c.cmd.Use +} + +func (c *CobraCommand) Short() string { + return c.cmd.Short +} + +func (c *CobraCommand) Example() string { + return c.cmd.Example +} + +func (c *CobraCommand) ObjectType() string { + if c.objname == "" { + return c.Name() + } + return c.objname +} + +func (c *CobraCommand) Verb() string { + return c.verb +} + +func (c *CobraCommand) Realm() string { + return c.realm +} + +func (c *CobraCommand) CLIConfigRequired() bool { + return c.cliConfigRequired +} + +func (c *CobraCommand) Command() *cobra.Command { + return c.cmd +} diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/app.go b/pkg/contexts/ocm/plugin/ppi/cmds/app.go index cc2ebf2d8e..87591f1e14 100644 --- a/pkg/contexts/ocm/plugin/ppi/cmds/app.go +++ b/pkg/contexts/ocm/plugin/ppi/cmds/app.go @@ -12,6 +12,7 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/accessmethod" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/action" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/command" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/describe" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/download" "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/info" @@ -44,6 +45,7 @@ func NewPluginCommand(p ppi.Plugin) *PluginCommand { Short: short, Long: p.Descriptor().Long, Version: p.Version(), + PersistentPreRunE: pcmd.PreRunE, TraverseChildren: true, SilenceUsage: true, DisableFlagsInUseLine: true, @@ -63,15 +65,11 @@ func NewPluginCommand(p ppi.Plugin) *PluginCommand { cmd.AddCommand(upload.New(p)) cmd.AddCommand(download.New(p)) cmd.AddCommand(valueset.New(p)) + cmd.AddCommand(command.New(p)) cmd.InitDefaultHelpCmd() - var help *cobra.Command - for _, c := range cmd.Commands() { - if c.Name() == "help" { - help = c - break - } - } + help := cobrautils.GetHelpCommand(cmd) + // help.Use="help " help.DisableFlagsInUseLine = true cmd.AddCommand(descriptor.New()) @@ -87,6 +85,13 @@ type Error struct { Error string `json:"error"` } +func (p *PluginCommand) PreRunE(cmd *cobra.Command, args []string) error { + if handler != nil { + return handler.HandleConfig(p.plugin.GetOptions().LogConfig) + } + return nil +} + func (p *PluginCommand) Execute(args []string) error { p.command.SetArgs(args) err := p.command.Execute() diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/command/cmd.go b/pkg/contexts/ocm/plugin/ppi/cmds/command/cmd.go new file mode 100644 index 0000000000..d55c5725d7 --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/cmds/command/cmd.go @@ -0,0 +1,66 @@ +package command + +import ( + "context" + "os" + + "github.com/spf13/cobra" + + "github.com/open-component-model/ocm/pkg/cobrautils" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/common" +) + +const ( + Name = "command" + OptCliConfig = common.OptCliConfig +) + +func New(p ppi.Plugin) *cobra.Command { + cmd := &cobra.Command{ + Use: Name, + Short: "CLI command extensions", + Long: `This command group provides all CLI command extensions +described by an access method descriptor (` + p.Name() + ` descriptor.`, + TraverseChildren: true, + } + var cliconfig string + cmd.Flags().StringVarP(&cliconfig, OptCliConfig, "", "", "path to cli configuration file") + + found := false + for _, n := range p.Commands() { + found = true + c := n.Command() + c.TraverseChildren = true + + nested := c.PreRunE + c.PreRunE = func(cmd *cobra.Command, args []string) error { + ctx, err := ConfigureFromFile(context.Background(), cliconfig) + if err != nil { + return err + } + c.SetContext(ctx) + if nested != nil { + return nested(cmd, args) + } + return nil + } + cmd.AddCommand(n.Command()) + } + if found { + cobrautils.TweakHelpCommandFor(cmd) + } + return cmd +} + +func ConfigureFromFile(ctx context.Context, path string) (context.Context, error) { + data, err := os.ReadFile(path) + if err != nil { + return ctx, err + } + + if handler != nil { + return handler.HandleConfig(ctx, data) + } + return ctx, nil +} diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/command/config.go b/pkg/contexts/ocm/plugin/ppi/cmds/command/config.go new file mode 100644 index 0000000000..60304f8d3e --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/cmds/command/config.go @@ -0,0 +1,19 @@ +package command + +import ( + "context" +) + +type CommandConfigHandler interface { + HandleConfig(ctx context.Context, data []byte) (context.Context, error) +} + +var handler CommandConfigHandler + +// RegisterCommandConfigHandler is used to register a configuration handler +// for OCM configuration passed by the OCM library. +// If the OCM config framework is , it can be adapted +// by adding the ananymous import of the ppi/config package. +func RegisterCommandConfigHandler(h CommandConfigHandler) { + handler = h +} diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/common/const.go b/pkg/contexts/ocm/plugin/ppi/cmds/common/const.go index b4451a419c..e58eaf96c6 100644 --- a/pkg/contexts/ocm/plugin/ppi/cmds/common/const.go +++ b/pkg/contexts/ocm/plugin/ppi/cmds/common/const.go @@ -1,9 +1,10 @@ package common const ( - OptCreds = "credentials" - OptHint = "hint" - OptMedia = "mediaType" - OptArt = "artifactType" - OptConfig = "config" + OptCreds = "credentials" + OptHint = "hint" + OptMedia = "mediaType" + OptArt = "artifactType" + OptConfig = "config" + OptCliConfig = "cli-config" ) diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/logging.go b/pkg/contexts/ocm/plugin/ppi/cmds/logging.go new file mode 100644 index 0000000000..7fe97c1ec4 --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/cmds/logging.go @@ -0,0 +1,29 @@ +package cmds + +import ( + "encoding/json" +) + +type LoggingHandler interface { + HandleConfig(data []byte) error +} + +var handler LoggingHandler + +// RegisterLoggingConfigHandler is used to register a configuration handler +// for logging configration passed by the OCM library. +// If standard mandelsoft logging is used, it can be adapted +// by adding the ananymous import of the ppi/logging package. +func RegisterLoggingConfigHandler(h LoggingHandler) { + handler = h +} + +// LoggingConfiguration describes logging configuration for a slave executables like +// plugins. +// If mandelsoft logging is used please use github.com/open-component-model/ocm/pkg/cobrautils/logging.LoggingConfiguration, +// instead. +type LoggingConfiguration struct { + LogFileName string `json:"logFileName"` + LogConfig json.RawMessage `json:"logConfig"` + Json bool `json:"json,omitempty"` +} diff --git a/pkg/contexts/ocm/plugin/ppi/config/config.go b/pkg/contexts/ocm/plugin/ppi/config/config.go new file mode 100644 index 0000000000..db16583689 --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/config/config.go @@ -0,0 +1,28 @@ +package config + +import ( + "context" + + "github.com/open-component-model/ocm/pkg/contexts/ocm" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds/command" + "github.com/open-component-model/ocm/pkg/runtime" +) + +func init() { + command.RegisterCommandConfigHandler(&commandHandler{}) +} + +type commandHandler struct{} + +func (c commandHandler) HandleConfig(ctx context.Context, data []byte) (context.Context, error) { + var err error + + octx := ocm.DefaultContext() + ctx = octx.BindTo(ctx) + if len(data) != 0 { + _, err = octx.ConfigContext().ApplyData(data, runtime.DefaultYAMLEncoding, " cli config") + // Ugly, enforce configuration update + octx.GetResolver() + } + return ctx, err +} diff --git a/pkg/contexts/ocm/plugin/ppi/config/doc.go b/pkg/contexts/ocm/plugin/ppi/config/doc.go new file mode 100644 index 0000000000..a971c02fe0 --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/config/doc.go @@ -0,0 +1,6 @@ +// The config package can be used if the plugin should provide +// the ocm configuration from the calling OCM library. +// Just add an anonymous import to this package to your main program +// or CLI extension command. +// The config is only passed, if a CLI extension caommand is called. +package config diff --git a/pkg/contexts/ocm/plugin/ppi/interface.go b/pkg/contexts/ocm/plugin/ppi/interface.go index 5946a078b9..0f46db5351 100644 --- a/pkg/contexts/ocm/plugin/ppi/interface.go +++ b/pkg/contexts/ocm/plugin/ppi/interface.go @@ -4,6 +4,9 @@ import ( "encoding/json" "io" + "github.com/spf13/cobra" + + "github.com/open-component-model/ocm/pkg/contexts/config/cpi" "github.com/open-component-model/ocm/pkg/contexts/credentials" "github.com/open-component-model/ocm/pkg/contexts/datacontext/action" "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/options" @@ -39,6 +42,7 @@ type Plugin interface { SetShort(s string) SetLong(s string) SetConfigParser(config func(raw json.RawMessage) (interface{}, error)) + ForwardLogging(b ...bool) RegisterDownloader(arttype, mediatype string, u Downloader) error GetDownloader(name string) Downloader @@ -64,6 +68,14 @@ type Plugin interface { DecodeValueSet(purpose string, data []byte) (runtime.TypedObject, error) GetValueSet(purpose, name, version string) ValueSet + RegisterCommand(c Command) error + GetCommand(name string) Command + Commands() []Command + + RegisterConfigType(c cpi.ConfigType) error + GetConfigType(name string) *descriptor.ConfigTypeDescriptor + ConfigTypes() []descriptor.ConfigTypeDescriptor + GetOptions() *Options GetConfig() (interface{}, error) } @@ -178,3 +190,31 @@ type ValueSet interface { ValidateSpecification(p Plugin, spec runtime.TypedObject) (info *ValueSetInfo, err error) ComposeSpecification(p Plugin, opts Config, config Config) error } + +// Command is the interface for a CLI command provided by a plugin. +type Command interface { + // Name of command used in the plugin. + // This is also the default object type and is used to + // name top-level commands in the CLI. + Name() string + Description() string + Usage() string + Short() string + Example() string + // ObjectType is optional and can be used + // together with a verb. It then is used as + // sub command name for the object type. + // By default, the command name is used. + ObjectType() string + // Verb is optional and can be set + // to place the command in the verb hierarchy of + // the OCM CLI. It is used together with the ObjectType. + // (command will be *ocm *. + Verb() string + // Realm is optional and is used to place the command + // in a realm. This requires a verb. + Realm() string + CLIConfigRequired() bool + + Command() *cobra.Command +} diff --git a/pkg/contexts/ocm/plugin/ppi/logging/config.go b/pkg/contexts/ocm/plugin/ppi/logging/config.go new file mode 100644 index 0000000000..598c2a2479 --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/logging/config.go @@ -0,0 +1,25 @@ +package logging + +import ( + "sigs.k8s.io/yaml" + + "github.com/open-component-model/ocm/pkg/cobrautils/logopts/logging" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/ppi/cmds" +) + +func init() { + cmds.RegisterLoggingConfigHandler(&loggingConfigHandler{}) +} + +type loggingConfigHandler struct{} + +func (l loggingConfigHandler) HandleConfig(data []byte) error { + var cfg logging.LoggingConfiguration + + err := yaml.Unmarshal(data, &cfg) + if err != nil { + return err + } + + return cfg.Apply() +} diff --git a/pkg/contexts/ocm/plugin/ppi/logging/doc.go b/pkg/contexts/ocm/plugin/ppi/logging/doc.go new file mode 100644 index 0000000000..3e9954a475 --- /dev/null +++ b/pkg/contexts/ocm/plugin/ppi/logging/doc.go @@ -0,0 +1,6 @@ +// The logging package can be used if the plugin should handle +// the ocm logging configuration from the calling OCM library. +// Just add an anonymous import to this package to your main program. +// The passed config used contains settings according to the mandelsoft logging +// package. +package logging diff --git a/pkg/contexts/ocm/plugin/ppi/options.go b/pkg/contexts/ocm/plugin/ppi/options.go index 7b7dbe0f53..77cffcf5b2 100644 --- a/pkg/contexts/ocm/plugin/ppi/options.go +++ b/pkg/contexts/ocm/plugin/ppi/options.go @@ -8,10 +8,17 @@ import ( "github.com/open-component-model/ocm/pkg/cobrautils/flag" ) +const ( + OptPluginConfig = "config" + OptPlugingLogConfig = "log-config" +) + type Options struct { - Config json.RawMessage + Config json.RawMessage + LogConfig json.RawMessage } func (o *Options) AddFlags(fs *pflag.FlagSet) { - flag.YAMLVarP(fs, &o.Config, "config", "c", nil, "plugin configuration") + flag.YAMLVarP(fs, &o.Config, OptPluginConfig, "c", nil, "plugin configuration") + flag.YAMLVarP(fs, &o.LogConfig, OptPlugingLogConfig, "", nil, "ocm logging configuration") } diff --git a/pkg/contexts/ocm/plugin/ppi/plugin.go b/pkg/contexts/ocm/plugin/ppi/plugin.go index aa6c5687fe..d324fb4ea9 100644 --- a/pkg/contexts/ocm/plugin/ppi/plugin.go +++ b/pkg/contexts/ocm/plugin/ppi/plugin.go @@ -5,8 +5,14 @@ import ( "fmt" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/goutils/generics" + "github.com/mandelsoft/goutils/maputils" + "github.com/spf13/cobra" "golang.org/x/exp/slices" + "github.com/open-component-model/ocm/pkg/cobrautils" + "github.com/open-component-model/ocm/pkg/contexts/config" "github.com/open-component-model/ocm/pkg/contexts/datacontext/action" "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/options" metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" @@ -40,6 +46,8 @@ type plugin struct { valuesets map[string]map[string]ValueSet setScheme map[string]runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]] + clicmds map[string]Command + configParser func(message json.RawMessage) (interface{}, error) } @@ -65,6 +73,8 @@ func NewPlugin(name string, version string) Plugin { valuesets: map[string]map[string]ValueSet{}, setScheme: map[string]runtime.Scheme[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]]{}, + clicmds: map[string]Command{}, + descriptor: descriptor.Descriptor{ Version: descriptor.VERSION, PluginName: name, @@ -108,6 +118,26 @@ func (p *plugin) SetConfigParser(config func(raw json.RawMessage) (interface{}, p.configParser = config } +func (p *plugin) ForwardLogging(b ...bool) { + p.descriptor.ForwardLogging = general.OptionalDefaultedBool(true, b...) +} + +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) +} + +//////////////////////////////////////////////////////////////////////////////// + func (p *plugin) RegisterDownloader(arttype, mediatype string, hdlr Downloader) error { key := DownloaderKey{}.SetArtifact(arttype, mediatype) if !key.IsValid() { @@ -165,6 +195,8 @@ func (p *plugin) GetDownloaderFor(arttype, mediatype string) Downloader { return h[0] } +//////////////////////////////////////////////////////////////////////////////// + func (p *plugin) RegisterRepositoryContextUploader(contexttype, repotype, arttype, mediatype string, u Uploader) error { if contexttype == "" || repotype == "" { return fmt.Errorf("repository context required") @@ -244,6 +276,8 @@ func (p *plugin) DecodeUploadTargetSpecification(data []byte) (UploadTargetSpec, return o, nil } +//////////////////////////////////////////////////////////////////////////////// + func (p *plugin) RegisterAccessMethod(m AccessMethod) error { if p.GetAccessMethod(m.Name(), m.Version()) != nil { n := m.Name() @@ -276,9 +310,11 @@ func (p *plugin) RegisterAccessMethod(m AccessMethod) error { if vers == "" { meth := descriptor.AccessMethodDescriptor{ ValueSetDefinition: descriptor.ValueSetDefinition{ - Name: m.Name(), - Description: m.Description(), - Format: m.Format(), + ValueTypeDefinition: descriptor.ValueTypeDefinition{ + Name: m.Name(), + Description: m.Description(), + Format: m.Format(), + }, }, SupportContentIdentity: idp, } @@ -289,11 +325,13 @@ func (p *plugin) RegisterAccessMethod(m AccessMethod) error { } meth := descriptor.AccessMethodDescriptor{ ValueSetDefinition: descriptor.ValueSetDefinition{ - Name: m.Name(), - Version: vers, - Description: m.Description(), - Format: m.Format(), - CLIOptions: optlist, + ValueTypeDefinition: descriptor.ValueTypeDefinition{ + Name: m.Name(), + Version: vers, + Description: m.Description(), + Format: m.Format(), + }, + CLIOptions: optlist, }, SupportContentIdentity: idp, } @@ -315,6 +353,8 @@ func (p *plugin) GetAccessMethod(name string, version string) AccessMethod { return p.methods[n] } +//////////////////////////////////////////////////////////////////////////////// + func (p *plugin) RegisterAction(a Action) error { if p.GetAction(a.Name()) != nil { return errors.ErrAlreadyExists("action", a.Name()) @@ -344,6 +384,8 @@ func (p *plugin) GetAction(name string) Action { return p.actions[name] } +//////////////////////////////////////////////////////////////////////////////// + func (p *plugin) RegisterValueMergeHandler(a ValueMergeHandler) error { if p.GetValueMergeHandler(a.Name()) != nil { return errors.ErrAlreadyExists("value mergehandler", a.Name()) @@ -383,19 +425,7 @@ func (p *plugin) GetLabelMergeSpecification(id string) *descriptor.LabelMergeSpe return p.mergespecs[id] } -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) -} +//////////////////////////////////////////////////////////////////////////////// func (p *plugin) DecodeValueSet(purpose string, data []byte) (runtime.TypedObject, error) { schemes := p.setScheme[purpose] @@ -450,9 +480,11 @@ func (p *plugin) RegisterValueSet(s ValueSet) error { if vers == "" { set := descriptor.ValueSetDescriptor{ ValueSetDefinition: descriptor.ValueSetDefinition{ - Name: s.Name(), - Description: s.Description(), - Format: s.Format(), + ValueTypeDefinition: descriptor.ValueTypeDefinition{ + Name: s.Name(), + Description: s.Description(), + Format: s.Format(), + }, }, Purposes: slices.Clone(s.Purposes()), } @@ -475,11 +507,13 @@ func (p *plugin) RegisterValueSet(s ValueSet) error { } set := descriptor.ValueSetDescriptor{ ValueSetDefinition: descriptor.ValueSetDefinition{ - Name: s.Name(), - Version: vers, - Description: s.Description(), - Format: s.Format(), - CLIOptions: optlist, + ValueTypeDefinition: descriptor.ValueTypeDefinition{ + Name: s.Name(), + Version: vers, + Description: s.Description(), + Format: s.Format(), + }, + CLIOptions: optlist, }, Purposes: slices.Clone(s.Purposes()), } @@ -500,3 +534,110 @@ func (p *plugin) RegisterValueSet(s ValueSet) error { } return nil } + +//////////////////////////////////////////////////////////////////////////////// + +func (p *plugin) GetCommand(name string) Command { + return p.clicmds[name] +} + +func (p *plugin) RegisterCommand(c Command) error { + if p.GetCommand(c.Name()) != nil { + return errors.ErrAlreadyExists("cli command spec", c.Name()) + } + if c.Realm() != "" && c.Verb() == "" { + return errors.Newf("realm requires verb") + } + cmd := c.Command() + if cmd.HasSubCommands() && c.Verb() != "" { + return errors.Newf("no sub commands allowd for CLI command for verb") + } + + objtype := c.ObjectType() + if objtype == c.Name() { + objtype = "" + } + p.descriptor.Commands = append(p.descriptor.Commands, descriptor.CommandDescriptor{ + Name: c.Name(), + Description: c.Description(), + Usage: c.Usage(), + Short: c.Short(), + Example: c.Example(), + Realm: c.Realm(), + ObjectType: objtype, + Verb: c.Verb(), + CLIConfigRequired: c.CLIConfigRequired(), + }) + + path := []string{"ocm"} + if c.Verb() != "" { + path = append(path, c.Verb(), c.ObjectType()) + cobrautils.SetCommandSubstitutionForTree(cmd, 3, path) + } else { + cobrautils.SetCommandSubstitutionForTree(cmd, 2, path) + } + + orig := cmd.HelpFunc() + cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + var err error + // look for arguments of the command. + // wrong args passed to help function, instead + // of the sub command args, the complete command line is + // passed. + _, args, err = cmd.Root().Traverse(args) + if len(args) > 0 && err == nil { + cmd, args, _ = cmd.Find(args) + } + orig(cmd, args) + }) + p.clicmds[c.Name()] = c + return nil +} + +func (p *plugin) Commands() []Command { + return maputils.OrderedValues(p.clicmds) +} + +//////////////////////////////////////////////////////////////////////////////// + +func (p *plugin) GetConfigType(name string) *descriptor.ConfigTypeDescriptor { + var def *descriptor.ConfigTypeDescriptor + for _, d := range p.descriptor.ConfigTypes { + v := d.Name + if d.Version != "" { + v += runtime.VersionSeparator + d.Version + } + if v == name { + return &d + } + if d.Name == name && (def == nil || d.Version == "v1") { + def = generics.Pointer(d) + } + } + return def +} + +func (p *plugin) RegisterConfigType(t config.ConfigType) error { + name := t.GetKind() + version := "" + if t.GetType() != t.GetKind() { + version = t.GetVersion() + } + if f := p.GetConfigType(t.GetType()); f != nil { + if version == f.Version { + return errors.ErrAlreadyExists("config type", t.GetType()) + } + } + + p.descriptor.ConfigTypes = append(p.descriptor.ConfigTypes, descriptor.ConfigTypeDescriptor{ + Name: name, + Version: version, + Description: t.Usage(), + // TODO: separate format and description + }) + return nil +} + +func (p *plugin) ConfigTypes() []descriptor.ConfigTypeDescriptor { + return slices.Clone(p.descriptor.ConfigTypes) +} diff --git a/pkg/contexts/ocm/plugin/testutils/plugintests.go b/pkg/contexts/ocm/plugin/testutils/plugintests.go new file mode 100644 index 0000000000..6bc3352b02 --- /dev/null +++ b/pkg/contexts/ocm/plugin/testutils/plugintests.go @@ -0,0 +1,31 @@ +package testutils + +import ( + "github.com/mandelsoft/goutils/testutils" + + "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/plugin/cache" + "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" +) + +type TempPluginDir = testutils.TempDir + +func ConfigureTestPlugins2(ctx ocm.ContextProvider, path string) (TempPluginDir, plugins.Set, error) { + t, err := ConfigureTestPlugins(ctx, path) + if err != nil { + return nil, nil, err + } + return t, plugincacheattr.Get(ctx), nil +} + +func ConfigureTestPlugins(ctx ocm.ContextProvider, path string) (TempPluginDir, error) { + t, err := testutils.NewTempDir(testutils.WithDirContent(path)) + if err != nil { + return nil, err + } + cache.DirectoryCache.Reset() + plugindirattr.Set(ctx.OCMContext(), t.Path()) + return t, nil +} diff --git a/pkg/contexts/ocm/registration/registration.go b/pkg/contexts/ocm/registration/registration.go index cf2281eac0..7cf9578c41 100644 --- a/pkg/contexts/ocm/registration/registration.go +++ b/pkg/contexts/ocm/registration/registration.go @@ -3,6 +3,7 @@ package registration import ( "golang.org/x/exp/slices" + "github.com/open-component-model/ocm/pkg/contexts/config/plugin" "github.com/open-component-model/ocm/pkg/contexts/datacontext/action" "github.com/open-component-model/ocm/pkg/contexts/datacontext/action/handlers" "github.com/open-component-model/ocm/pkg/contexts/ocm" @@ -135,6 +136,19 @@ func RegisterExtensions(ctxp ocm.ContextProvider) error { vmreg.AssignHandler(hpi.LabelHint(s.Name, s.Version), &s.MergeAlgorithmSpecification) } } + + registry := ctx.ConfigContext().ConfigTypes() + for _, s := range p.GetDescriptor().ConfigTypes { + name := s.Name + if s.Version != "" { + name += runtime.VersionSeparator + s.Version + } + if registry.GetType(name) != nil { + logger.Error("config type {{type}} already registered", "type", name) + } + t := plugin.New(name, s.Description) + registry.Register(t) + } } return nil } diff --git a/pkg/contexts/ocm/signing/signing_test.go b/pkg/contexts/ocm/signing/signing_test.go index d6c47d8ce5..343d206592 100644 --- a/pkg/contexts/ocm/signing/signing_test.go +++ b/pkg/contexts/ocm/signing/signing_test.go @@ -179,7 +179,7 @@ applying to version "github.com/mandelsoft/test:v1"[github.com/mandelsoft/test:v digestA := "01de99400030e8336020059a435cea4e7fe8f21aad4faf619da882134b85569d" digestB := "5f416ec59629d6af91287e2ba13c6360339b6a0acf624af2abd2a810ce4aefce" - localDigests := common.Properties{ + localDigests := Substitutions{ "D_COMPA": digestA, "D_COMPB": digestB, } @@ -522,7 +522,7 @@ applying to version "github.com/mandelsoft/ref:v1"[github.com/mandelsoft/ref:v1] D_COMPC := "b376a7b440c0b1e506e54a790966119a8e229cf9226980b84c628d77ef06fc58" D_COMPD := "64674d3e2843d36c603f44477e4cd66ee85fe1a91227bbcd271202429024ed61" - localDigests := common.Properties{ + localDigests := Substitutions{ "D_DATAA": D_DATAA, "D_DATAB": D_DATAB, "D_DATAB512": D_DATAB512, @@ -1349,7 +1349,7 @@ const wrongDigest = "0a835d52867572bdaf7da7fb35ee59ad45c3db2dacdeeca62178edd5d07 type EntryCheck interface { Mode() string - Check1CheckD(cvd ocm.ComponentVersionAccess, d common.Properties) + Check1CheckD(cvd ocm.ComponentVersionAccess, d Substitutions) Check1CheckA(cva ocm.ComponentVersionAccess, d *metav1.DigestSpec, mopts ...ocm.ModificationOption) Check1Corrupt(cva ocm.ComponentVersionAccess, f *Finalizer, cvd ocm.ComponentVersionAccess) @@ -1362,7 +1362,7 @@ func (*EntryLocal) Mode() string { return DIGESTMODE_LOCAL } -func (*EntryLocal) Check1CheckD(cvd ocm.ComponentVersionAccess, _ common.Properties) { +func (*EntryLocal) Check1CheckD(cvd ocm.ComponentVersionAccess, _ Substitutions) { ExpectWithOffset(1, cvd.GetDescriptor().NestedDigests).To(BeNil()) } @@ -1392,7 +1392,7 @@ func (*EntryTop) Mode() string { return DIGESTMODE_TOP } -func (*EntryTop) Check1CheckD(cvd ocm.ComponentVersionAccess, digests common.Properties) { +func (*EntryTop) Check1CheckD(cvd ocm.ComponentVersionAccess, digests Substitutions) { ExpectWithOffset(1, cvd.GetDescriptor().NestedDigests).NotTo(BeNil()) ExpectWithOffset(1, cvd.GetDescriptor().NestedDigests.String()).To(StringEqualTrimmedWithContext(` github.com/mandelsoft/ref:v1: SHA-256:${D_COMPB}[jsonNormalisation/v1] diff --git a/pkg/contexts/ocm/testhelper/resources.go b/pkg/contexts/ocm/testhelper/resources.go index c3f71361c3..8e7b9486a6 100644 --- a/pkg/contexts/ocm/testhelper/resources.go +++ b/pkg/contexts/ocm/testhelper/resources.go @@ -1,7 +1,8 @@ package testhelper import ( - "github.com/open-component-model/ocm/pkg/common" + "github.com/mandelsoft/goutils/testutils" + metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" "github.com/open-component-model/ocm/pkg/contexts/ocm/digester/digesters/blob" "github.com/open-component-model/ocm/pkg/env/builder" @@ -17,7 +18,7 @@ func TextResourceDigestSpec(d string) *metav1.DigestSpec { } } -var Digests = common.Properties{ +var Digests = testutils.Substitutions{ "D_TESTDATA": D_TESTDATA, "D_OTHERDATA": D_OTHERDATA, } diff --git a/pkg/contexts/ocm/utils/configure.go b/pkg/contexts/ocm/utils/configure.go index 574eaab037..68d0479007 100644 --- a/pkg/contexts/ocm/utils/configure.go +++ b/pkg/contexts/ocm/utils/configure.go @@ -6,11 +6,13 @@ import ( "strings" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/pkgutils" "github.com/mandelsoft/spiff/features" "github.com/mandelsoft/spiff/spiffing" "github.com/mandelsoft/vfs/pkg/vfs" "github.com/open-component-model/ocm/pkg/contexts/config" + configcfg "github.com/open-component-model/ocm/pkg/contexts/config/config" "github.com/open-component-model/ocm/pkg/contexts/ocm" "github.com/open-component-model/ocm/pkg/contexts/ocm/utils/defaultconfigregistry" "github.com/open-component-model/ocm/pkg/utils" @@ -20,9 +22,18 @@ const DEFAULT_OCM_CONFIG = ".ocmconfig" const DEFAULT_OCM_CONFIG_DIR = ".ocm" -func Configure(ctx config.ContextProvider, path string, fss ...vfs.FileSystem) (ocm.Context, error) { +func Configure(ctxp config.ContextProvider, path string, fss ...vfs.FileSystem) (ocm.Context, error) { + ctx, _, err := Configure2(ctxp, path, fss...) + return ctx, err +} + +func Configure2(ctx config.ContextProvider, path string, fss ...vfs.FileSystem) (ocm.Context, config.Config, error) { var ocmctx ocm.Context + cfg, err := configcfg.NewAggregator(false) + if err != nil { + return nil, nil, err + } fs := utils.FileSystem(fss...) if ctx == nil { ocmctx = ocm.DefaultContext() @@ -54,44 +65,68 @@ func Configure(ctx config.ContextProvider, path string, fss ...vfs.FileSystem) ( if path != "" && path != "None" { if strings.HasPrefix(path, "~"+string(os.PathSeparator)) { if len(h) == 0 { - return nil, fmt.Errorf("no home directory found for resolving path of ocm config file %q", path) + return nil, nil, fmt.Errorf("no home directory found for resolving path of ocm config file %q", path) } path = h + path[1:] } data, err := vfs.ReadFile(fs, path) if err != nil { - return nil, errors.Wrapf(err, "cannot read ocm config file %q", path) + return nil, nil, errors.Wrapf(err, "cannot read ocm config file %q", path) } - if err = ConfigureByData(ctx, data, path); err != nil { - return nil, err + if eff, err := ConfigureByData2(ctx, data, path); err != nil { + return nil, nil, err + } else { + err = cfg.AddConfig(eff) + if err != nil { + return nil, nil, err + } } } else { for _, h := range defaultconfigregistry.Get() { - err := h(ctx.ConfigContext()) + desc, def, err := h(ctx.ConfigContext()) if err != nil { - return nil, err + return nil, nil, err + } + if def != nil { + name, err := pkgutils.GetPackageName(h) + if err != nil { + name = "unknown handler" + } + err = ctx.ConfigContext().ApplyConfig(def, fmt.Sprintf("%s: %s", name, desc)) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot apply default config from %s(%s)", name, desc) + } + err = cfg.AddConfig(def) + if err != nil { + return nil, nil, err + } } } } - return ocmctx, nil + return ocmctx, cfg.Get(), nil } func ConfigureByData(ctx config.ContextProvider, data []byte, info string) error { + _, err := ConfigureByData2(ctx, data, info) + return err +} + +func ConfigureByData2(ctx config.ContextProvider, data []byte, info string) (config.Config, error) { var err error sctx := spiffing.New().WithFeatures(features.INTERPOLATION, features.CONTROL) data, err = spiffing.Process(sctx, spiffing.NewSourceData(info, data)) if err != nil { - return errors.Wrapf(err, "processing ocm config %q", info) + return nil, errors.Wrapf(err, "processing ocm config %q", info) } cfg, err := ctx.ConfigContext().GetConfigForData(data, nil) if err != nil { - return errors.Wrapf(err, "invalid ocm config file %q", info) + return nil, errors.Wrapf(err, "invalid ocm config file %q", info) } err = ctx.ConfigContext().ApplyConfig(cfg, info) if err != nil { - return errors.Wrapf(err, "cannot apply ocm config %q", info) + return nil, errors.Wrapf(err, "cannot apply ocm config %q", info) } - return nil + return cfg, nil } diff --git a/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go b/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go index cce4170c7d..faf06f3d4f 100644 --- a/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go +++ b/pkg/contexts/ocm/utils/defaultconfigregistry/configure.go @@ -8,7 +8,7 @@ import ( "github.com/open-component-model/ocm/pkg/listformat" ) -type DefaultConfigHandler func(cfg config.Context) error +type DefaultConfigHandler func(cfg config.Context) (string, config.Config, error) type defaultConfigurationRegistry struct { lock sync.Mutex diff --git a/pkg/contexts/ocm/valuemergehandler/handlers/plugin/handler_test.go b/pkg/contexts/ocm/valuemergehandler/handlers/plugin/handler_test.go index bad5ca3ad0..930dc5350d 100644 --- a/pkg/contexts/ocm/valuemergehandler/handlers/plugin/handler_test.go +++ b/pkg/contexts/ocm/valuemergehandler/handlers/plugin/handler_test.go @@ -6,10 +6,10 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" . "github.com/open-component-model/ocm/pkg/env/builder" "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/registration" "github.com/open-component-model/ocm/pkg/contexts/ocm/valuemergehandler" "github.com/open-component-model/ocm/pkg/contexts/ocm/valuemergehandler/handlers/defaultmerge" @@ -24,16 +24,18 @@ const ( var _ = Describe("plugin value merge handler", func() { var ctx ocm.Context var env *Builder + var plugins TempPluginDir var registry valuemergehandler.Registry BeforeEach(func() { env = NewBuilder(nil) ctx = env.OCMContext() - plugindirattr.Set(ctx, "testdata") + plugins = Must(ConfigureTestPlugins(ctx, "testdata")) registry = valuemergehandler.For(ctx) }) AfterEach(func() { + plugins.Cleanup() env.Cleanup() }) diff --git a/pkg/env/env.go b/pkg/env/env.go index 65d4e5bb20..ee9d08340f 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -64,30 +64,30 @@ type OptionHandler interface { Propagate(e *Environment) } -type dummyOptionHandler struct{} +type DefaultOptionHandler struct{} -var _ OptionHandler = (*dummyOptionHandler)(nil) +var _ OptionHandler = (*DefaultOptionHandler)(nil) -func (o dummyOptionHandler) Propagate(e *Environment) { +func (o DefaultOptionHandler) Propagate(e *Environment) { } -func (o dummyOptionHandler) OCMContext() ocm.Context { +func (o DefaultOptionHandler) OCMContext() ocm.Context { return nil } -func (o dummyOptionHandler) GetFilesystem() vfs.FileSystem { +func (o DefaultOptionHandler) GetFilesystem() vfs.FileSystem { return nil } -func (o dummyOptionHandler) GetFailHandler() FailHandler { +func (o DefaultOptionHandler) GetFailHandler() FailHandler { return nil } -func (o dummyOptionHandler) GetEnvironment() *Environment { +func (o DefaultOptionHandler) GetEnvironment() *Environment { return nil } -func (dummyOptionHandler) Mount(*composefs.ComposedFileSystem) error { +func (DefaultOptionHandler) Mount(*composefs.ComposedFileSystem) error { return nil } @@ -125,7 +125,7 @@ func (FailHandler) Propagate(e *Environment) { //////////////////////////////////////////////////////////////////////////////// type fsOpt struct { - dummyOptionHandler + DefaultOptionHandler path string fs vfs.FileSystem } @@ -158,7 +158,7 @@ func (o fsOpt) Mount(cfs *composefs.ComposedFileSystem) error { //////////////////////////////////////////////////////////////////////////////// type ctxOpt struct { - dummyOptionHandler + DefaultOptionHandler ctx ocm.Context } @@ -179,7 +179,7 @@ func (o ctxOpt) OCMContext() ocm.Context { //////////////////////////////////////////////////////////////////////////////// type propOpt struct { - dummyOptionHandler + DefaultOptionHandler } func UseAsContextFileSystem() Option { @@ -197,7 +197,7 @@ func (o ctxOpt) Propagate(e *Environment) { //////////////////////////////////////////////////////////////////////////////// type tdOpt struct { - dummyOptionHandler + DefaultOptionHandler path string source string modifiable bool @@ -306,7 +306,7 @@ func (o tdOpt) Mount(cfs *composefs.ComposedFileSystem) error { //////////////////////////////////////////////////////////////////////////////// type envOpt struct { - dummyOptionHandler + DefaultOptionHandler env *Environment } diff --git a/pkg/filelock/lock.go b/pkg/filelock/lock.go new file mode 100644 index 0000000000..4fe4ce641f --- /dev/null +++ b/pkg/filelock/lock.go @@ -0,0 +1,148 @@ +package filelock + +import ( + "io" + "os" + "sync" + + "github.com/juju/fslock" + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" +) + +const DIRECTORY_LOCK = ".lock" + +// Mutex is a lock object based on a file. +// It features a process lock and an +// in-process mutex. Therefore, it can be used +// in a GO program in multiple Go-routines to achieve a +// global synchronization among multiple processes +// on the same machine. +// The result of a Lock operation is an io.Closer +// which is used to release the lock again. +type Mutex struct { + lock sync.Mutex + path string + lockfile *fslock.Lock +} + +func (m *Mutex) Lock() (io.Closer, error) { + m.lock.Lock() + if m.lockfile == nil { + m.lockfile = fslock.New(m.path) + } + err := m.lockfile.Lock() + if err != nil { + m.lock.Unlock() + return nil, err + } + return &lock{mutex: m}, nil +} + +func (m *Mutex) TryLock() (io.Closer, error) { + if !m.lock.TryLock() { + return nil, nil + } + if m.lockfile == nil { + m.lockfile = fslock.New(m.path) + } + err := m.lockfile.TryLock() + if err != nil { + m.lock.Unlock() + if errors.Is(err, fslock.ErrLocked) { + err = nil + } + return nil, err + } + return &lock{mutex: m}, nil +} + +func (m *Mutex) Path() string { + return m.path +} + +type lock struct { + lock sync.Mutex + mutex *Mutex +} + +func (l *lock) Close() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.mutex == nil { + return os.ErrClosed + } + l.mutex.lockfile.Unlock() + l.mutex.lock.Unlock() + l.mutex = nil + return nil +} + +var ( + _filelocks = map[string]*Mutex{} + _lock sync.Mutex +) + +// MutexFor provides a canonical lock +// for the given file. If the file path describes +// a directory, the lock will be the file .lock +// inside this directory. +func MutexFor(path string) (*Mutex, error) { + ok, err := vfs.Exists(osfs.OsFs, path) + if err != nil { + return nil, err + } + + var file string + if ok { + file, err = vfs.Canonical(osfs.OsFs, path, true) + if err != nil { + return nil, err + } + ok, err = vfs.IsDir(osfs.OsFs, path) + if ok { + file = filepath.Join(file, DIRECTORY_LOCK) + } + } else { + // canonical path is canonical path of directory plus base name of path + dir := filepath.Dir(path) + dir, err = vfs.Canonical(osfs.OsFs, dir, true) + if err == nil { + file = filepath.Join(dir, filepath.Base(path)) + } + } + if err != nil { + return nil, err + } + + _lock.Lock() + defer _lock.Unlock() + + mutex := _filelocks[file] + if mutex == nil { + mutex = &Mutex{ + path: file, + } + _filelocks[file] = mutex + } + return mutex, nil +} + +func LockDir(dir string) (io.Closer, error) { + m, err := MutexFor(filepath.Join(dir, ".lock")) + if err != nil { + return nil, err + } + return m.Lock() +} + +func Lock(path string) (io.Closer, error) { + m, err := MutexFor(path) + if err != nil { + return nil, err + } + return m.Lock() +} diff --git a/pkg/filelock/lock_test.go b/pkg/filelock/lock_test.go new file mode 100644 index 0000000000..0471adc20e --- /dev/null +++ b/pkg/filelock/lock_test.go @@ -0,0 +1,36 @@ +package filelock_test + +import ( + "os" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/open-component-model/ocm/pkg/filelock" +) + +var _ = Describe("lock identity", func() { + It("identity", func() { + + l1 := Must(filelock.MutexFor("testdata/lock")) + l2 := Must(filelock.MutexFor("testdata/../testdata/lock")) + Expect(l1).To(BeIdenticalTo(l2)) + + Expect(filepath.Base(l1.Path())).To(Equal("lock")) + }) + + It("try lock", func() { + l := Must(filelock.MutexFor("testdata/lock")) + + c := Must(l.Lock()) + ExpectError(l.TryLock()).To(BeNil()) + c.Close() + ExpectError(c.Close()).To(BeIdenticalTo(os.ErrClosed)) + c = Must(l.TryLock()) + Expect(c).NotTo(BeNil()) + c.Close() + }) + +}) diff --git a/pkg/filelock/suite_test.go b/pkg/filelock/suite_test.go new file mode 100644 index 0000000000..df9af2807f --- /dev/null +++ b/pkg/filelock/suite_test.go @@ -0,0 +1,13 @@ +package filelock_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "filelock Test Suite") +} diff --git a/pkg/filelock/testdata/lock b/pkg/filelock/testdata/lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 525290efc7..565d37ae44 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" "github.com/mandelsoft/logging" logcfg "github.com/mandelsoft/logging/config" "github.com/opencontainers/go-digest" @@ -21,12 +22,15 @@ type StaticContext struct { lock sync.Mutex } -func NewContext(ctx logging.Context) *StaticContext { +func NewContext(ctx logging.Context, global ...bool) *StaticContext { if ctx == nil { ctx = logging.DefaultContext() } + if !general.Optional(global...) { + ctx = ctx.WithContext(REALM) + } return &StaticContext{ - Context: ctx.WithContext(REALM), + Context: ctx, applied: map[string]struct{}{}, } } @@ -53,15 +57,18 @@ func (s *StaticContext) Configure(config *logcfg.Config, extra ...string) error return nil } s.applied[d] = struct{}{} - return logcfg.Configure(logContext, config) + return logcfg.Configure(s.Context, config) } // global is a wrapper for the default global log content. -var global = NewContext(nil) +var global = NewContext(nil, true) + +// ocm is a wrapper for the default ocm log content. +var ocm = NewContext(nil) -// logContext is the global ocm log context. +// logContext is the ocm log context. // It can be replaced by SetContext. -var logContext = global +var logContext = ocm // SetContext sets a new preconfigured context. // This function should be called prior to any configuration @@ -98,6 +105,12 @@ func Configure(config *logcfg.Config, extra ...string) error { return logContext.Configure(config, extra...) } +// ConfigureOCM applies configuration for the default global ocm log context +// provided by this package. +func ConfigureOCM(config *logcfg.Config, extra ...string) error { + return ocm.Configure(config, extra...) +} + // ConfigureGlobal applies configuration for the default global log context // provided by this package. func ConfigureGlobal(config *logcfg.Config, extra ...string) error {