diff --git a/cmds/ocm/app/app.go b/cmds/ocm/app/app.go index 6b21b5ea63..00e08535dd 100644 --- a/cmds/ocm/app/app.go +++ b/cmds/ocm/app/app.go @@ -35,6 +35,7 @@ import ( "github.com/open-component-model/ocm/cmds/ocm/commands/toicmds" "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" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/clean" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/controller" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs/create" @@ -81,7 +82,7 @@ type CLIOptions struct { keyoption.Option Completed bool - Config string + Config []string ConfigSets []string Credentials []string Context clictx.Context @@ -221,6 +222,7 @@ func newCliCommand(opts *CLIOptions, mod ...func(clictx.Context, *cobra.Command) cmd.AddCommand(NewVersionCommand(opts.Context)) + cmd.AddCommand(check.NewCommand(opts.Context)) cmd.AddCommand(get.NewCommand(opts.Context)) cmd.AddCommand(create.NewCommand(opts.Context)) cmd.AddCommand(add.NewCommand(opts.Context)) @@ -296,7 +298,7 @@ func newCliCommand(opts *CLIOptions, mod ...func(clictx.Context, *cobra.Command) } func (o *CLIOptions) AddFlags(fs *pflag.FlagSet) { - fs.StringVarP(&o.Config, "config", "", "", "configuration file") + 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") @@ -325,9 +327,18 @@ func (o *CLIOptions) Complete() error { if err != nil { return err } - _, err = utils.Configure(o.Context.OCMContext(), o.Config, vfsattr.Get(o.Context)) - if err != nil { - return err + + 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) diff --git a/cmds/ocm/commands/common/options/failonerroroption/option.go b/cmds/ocm/commands/common/options/failonerroroption/option.go index 0da605fe99..47b7ab0def 100644 --- a/cmds/ocm/commands/common/options/failonerroroption/option.go +++ b/cmds/ocm/commands/common/options/failonerroroption/option.go @@ -5,6 +5,7 @@ package failonerroroption import ( + "github.com/open-component-model/ocm/pkg/errors" "github.com/spf13/pflag" "github.com/open-component-model/ocm/cmds/ocm/pkg/options" @@ -22,10 +23,40 @@ func New() *Option { type Option struct { Fail bool + err error } func (o *Option) AddFlags(fs *pflag.FlagSet) { - fs.BoolVarP(&o.Fail, "fail-on-error", "", false, "fail on label validation error") + fs.BoolVarP(&o.Fail, "fail-on-error", "", false, "fail on validation error") } var _ options.Options = (*Option)(nil) + +func (o *Option) GetError() error { + return o.err +} +func (o *Option) SetError(err error) { + o.err = err +} + +func (o *Option) AddError(err error) { + if err == nil { + return + } + if o.err == nil { + o.err = errors.ErrList().Add(err) + } else { + if l, ok := o.err.(*errors.ErrorList); ok { + l.Add(err) + } else { + o.err = errors.ErrList().Add(o.err, err) + } + } +} + +func (o *Option) ActivatedError() error { + if o.Fail { + return o.err + } + return nil +} diff --git a/cmds/ocm/commands/misccmds/credentials/get/cmd.go b/cmds/ocm/commands/misccmds/credentials/get/cmd.go index 02311b4ac0..e1649c8066 100644 --- a/cmds/ocm/commands/misccmds/credentials/get/cmd.go +++ b/cmds/ocm/commands/misccmds/credentials/get/cmd.go @@ -8,9 +8,6 @@ import ( "sort" "strings" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "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" @@ -19,6 +16,9 @@ import ( "github.com/open-component-model/ocm/pkg/contexts/credentials" "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/listformat" + "github.com/open-component-model/ocm/pkg/out" + "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -32,7 +32,8 @@ type Command struct { Consumer credentials.ConsumerIdentity Matcher credentials.IdentityMatcher - Type string + Type string + Sloppy bool } var _ utils.OCMCommand = (*Command)(nil) @@ -75,6 +76,7 @@ The usage of a dedicated matcher can be enforced by the option --matcher] {}", + Short: "Check completeness of a component version in an OCM repository", + Long: ` +This command checks, whether component versions are completely contained +in an OCM repository with all its dependent component references. +`, + Example: ` +$ ocm check componentversion ghcr.io/mandelsoft/kubelink +$ ocm get componentversion --repo OCIRegistry::ghcr.io mandelsoft/kubelink +`, + } +} + +func (o *Command) Complete(args []string) error { + o.Refs = args + if len(args) == 0 && repooption.From(o).Spec == "" { + return fmt.Errorf("a repository or at least one argument that defines the reference is needed") + } + return nil +} + +func (o *Command) Run() error { + session := ocm.NewSession(nil) + defer session.Close() + + err := o.ProcessOnOptions(ocmcommon.CompleteOptionsWithSession(o, session)) + if err != nil { + return err + } + handler := comphdlr.NewTypeHandler(o.Context.OCM(), session, repooption.From(o).Repository, comphdlr.OptionsFor(o)) + err = utils.HandleArgs(output.From(o), handler, o.Refs...) + if err != nil { + return err + } + return failonerroroption.From(o).ActivatedError() +} + +//////////////////////////////////////////////////////////////////////////////// + +var outputs = output.NewOutputs(OutputFactory(mapRegularOutput), output.Outputs{ + "wide": OutputFactory(mapWideOutput, "MISSING", "NON-LOCAL"), +}).AddChainedManifestOutputs(NewAction) + +func OutputFactory(fmt processing.MappingFunction, wide ...string) output.OutputFactory { + return func(opts *output.Options) output.Output { + return (&output.TableOutput{ + Headers: output.Fields("COMPONENT", "VERSION", "STATUS", "ERROR", wide), + Options: opts, + Chain: NewAction(opts), + Mapping: fmt, + }).New() + } +} + +func mapRegularOutput(e interface{}) interface{} { + p := e.(*Entry) + + err := "" + if p.Error != nil { + err = p.Error.Error() + } + return []string{p.ComponentVersion.GetName(), p.ComponentVersion.GetVersion(), p.Status, err} +} + +func mapWideOutput(e interface{}) interface{} { + p := e.(*Entry) + + line := mapRegularOutput(e).([]string) + if p.Results.IsEmpty() { + return append(line, "") + } + + mmsg := "" + amsg := "" + if len(p.Results.Missing) > 0 { + missing := map[string]string{} + for id, m := range p.Results.Missing { + sep := "[" + d := "" + for _, id := range m[:len(m)-1] { + d = d + sep + id.String() + sep = "->" + } + missing[id.String()] = d + "]" + } + sep := "" + for _, k := range utils2.StringMapKeys(missing) { + mmsg += sep + k + missing[k] + sep = ", " + } + } + + if len(p.Results.Resources) > 0 { + sep := "RSC(" + for _, r := range p.Results.Resources { + amsg = fmt.Sprintf("%s%s%s", amsg, sep, r.String()) + sep = "," + } + amsg += ")" + } + if len(p.Results.Sources) > 0 { + sep := "SRC(" + for _, r := range p.Results.Sources { + amsg = fmt.Sprintf("%s%s%s", amsg, sep, r.String()) + sep = "," + } + amsg += ")" + } + + return append(line, mmsg, amsg) +} + +//////////////////////////////////////////////////////////////////////////////// + +type CheckResult struct { + Missing Missing `json:"missing,omitempty"` + Resources []metav1.Identity `json:"resources,omitempty"` + Sources []metav1.Identity `json:"sources,omitempty"` +} + +func newCheckResult() *CheckResult { + return &CheckResult{Missing: Missing{}} +} + +func (r *CheckResult) IsEmpty() bool { + if r == nil { + return true + } + return len(r.Missing) == 0 && len(r.Resources) == 0 && len(r.Sources) == 0 +} + +type Missing map[common.NameVersion]common.History + +func (n Missing) MarshalJSON() ([]byte, error) { + m := map[string]common.History{} + for k, v := range n { + m[k.String()] = v + } + return json.Marshal(m) +} + +type Entry struct { + Status string `json:"status"` + ComponentVersion common.NameVersion `json:"componentVersion"` + Results *CheckResult `json:"missing,omitempty"` + Error error `json:"error,omitempty"` +} + +func (n CheckResult) MarshalJSON() ([]byte, error) { + m := map[string]common.History{} + for k, v := range n.Missing { + m[k.String()] = v + } + return json.Marshal(m) +} + +type action struct { + erropt *failonerroroption.Option + options *Option +} + +func NewAction(opts *output.Options) processing.ProcessChain { + return comphdlr.Sort.Map((&action{ + erropt: failonerroroption.From(opts), + options: From(opts), + }).Map) +} + +type Cache = map[common.NameVersion]*CheckResult + +func (a *action) Map(in interface{}) interface{} { + cache := Cache{} + + i := in.(*comphdlr.Object) + o := &Entry{ + ComponentVersion: common.VersionedElementKey(i.ComponentVersion), + } + status := "" + o.Results, o.Error = a.handle(cache, i.ComponentVersion, common.History{common.VersionedElementKey(i.ComponentVersion)}) + if o.Error != nil { + status = ",Error" + a.erropt.AddError(o.Error) + } + if !o.Results.IsEmpty() { + if len(o.Results.Missing) > 0 { + a.erropt.AddError(fmt.Errorf("incomplete component version %s", common.VersionedElementKey(i.ComponentVersion))) + status += ",Incomplete" + } + if len(o.Results.Sources) > 0 || len(o.Results.Resources) > 0 { + if len(o.Results.Resources) > 0 { + status += ",Resources" + a.erropt.AddError(fmt.Errorf("version %s with non-local resources", common.VersionedElementKey(i.ComponentVersion))) + } + if len(o.Results.Sources) > 0 { + status += ",Sources" + a.erropt.AddError(fmt.Errorf("version %s with non-local sources", common.VersionedElementKey(i.ComponentVersion))) + } + } + } + if status != "" { + o.Status = status[1:] + } else { + o.Status = "OK" + } + return o +} + +func (a *action) check(cache Cache, repo ocm.Repository, id common.NameVersion, h common.History) (*CheckResult, error) { + if r, ok := cache[id]; ok { + return r, nil + } + + err := h.Add(ocm.KIND_COMPONENTVERSION, id) + if err != nil { + return nil, err + } + cv, err := repo.LookupComponentVersion(id.GetName(), id.GetVersion()) + if err != nil { + if !errors.IsErrNotFound(err) { + return nil, err + } + err = nil + } + var r *CheckResult + if cv == nil { + r = &CheckResult{Missing: Missing{id: h}} + } else { + r, err = a.handle(cache, cv, h) + } + cache[id] = r + return r, err +} + +func (a *action) handle(cache Cache, cv ocm.ComponentVersionAccess, h common.History) (*CheckResult, error) { + result := newCheckResult() + + for _, r := range cv.GetDescriptor().References { + id := common.NewNameVersion(r.ComponentName, r.Version) + n, err := a.check(cache, cv.Repository(), id, h) + if err != nil { + return result, err + } + if n != nil && len(n.Missing) > 0 { + for k, v := range n.Missing { + result.Missing[k] = v + } + } + } + + var err error + + list := errors.ErrorList{} + if a.options.CheckLocalResources { + result.Resources, err = a.checkArtifacts(cv.GetContext(), cv.GetDescriptor().Resources) + list.Add(err) + } + if a.options.CheckLocalSources { + result.Sources, err = a.checkArtifacts(cv.GetContext(), cv.GetDescriptor().Sources) + list.Add(err) + } + if result.IsEmpty() { + result = nil + } + return result, list.Result() +} + +func (a *action) checkArtifacts(ctx ocm.Context, accessor compdesc.ElementAccessor) ([]metav1.Identity, error) { + var result []metav1.Identity + + list := errors.ErrorList{} + for i := 0; i < accessor.Len(); i++ { + e := accessor.Get(i).(compdesc.ElementArtifactAccessor) + + m, err := ctx.AccessSpecForSpec(e.GetAccess()) + if err != nil { + list.Add(err) + } else { + if !m.IsLocal(ctx) { + result = append(result, e.GetMeta().GetIdentity(accessor)) + } + } + } + return result, list.Result() +} diff --git a/cmds/ocm/commands/ocmcmds/components/check/cmd_test.go b/cmds/ocm/commands/ocmcmds/components/check/cmd_test.go new file mode 100644 index 0000000000..c0602d7424 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/check/cmd_test.go @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package check_test + +import ( + "bytes" + + . "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" + "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/ociartifact" + v1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + "github.com/open-component-model/ocm/pkg/contexts/ocm/resourcetypes" + . "github.com/open-component-model/ocm/pkg/testutils" + + "github.com/open-component-model/ocm/pkg/common/accessio" +) + +const ARCH = "/tmp/ca" +const VERSION = "v1" +const COMP = "test.de/x" +const COMP2 = "test.de/y" +const COMP3 = "test.de/z" +const COMP4 = "test.de/a" + +var _ = Describe("Test Environment", func() { + var env *TestEnv + + BeforeEach(func() { + env = NewTestEnv() + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("get checks refereces", func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMP, VERSION, func() { + env.Reference("ref", COMP3, VERSION) + }) + env.ComponentVersion(COMP2, VERSION, func() { + env.Reference("ref", COMP3, VERSION) + }) + env.ComponentVersion(COMP3, VERSION) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/x v1 OK +`)) + }) + + Context("finds missing", func() { + BeforeEach(func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMP, VERSION, func() { + env.Reference("ref", COMP3, VERSION) + }) + env.ComponentVersion(COMP2, VERSION, func() { + env.Reference("ref", COMP3, VERSION) + }) + }) + }) + + It("outputs table", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/x v1 Incomplete +`)) + }) + + It("outputs wide table", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP, "-o", "wide")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR MISSING NON-LOCAL +test.de/x v1 Incomplete test.de/z:v1[test.de/x:v1] +`)) + }) + + It("outputs json", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP, "-o", "json")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +{ + "items": [ + { + "status": "Incomplete", + "componentVersion": "test.de/x:v1", + "missing": { + "test.de/z:v1": [ + "test.de/x:v1", + "test.de/z:v1" + ] + } + } + ] +} +`)) + }) + + It("provides error table", func() { + buf := bytes.NewBuffer(nil) + ExpectError(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP, "--fail-on-error")). + To(MatchError("incomplete component version test.de/x:v1")) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/x v1 Incomplete +`)) + }) + }) + + It("handles diamond", func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMP, VERSION, func() { + env.Reference("ref1", COMP2, VERSION) + env.Reference("ref2", COMP3, VERSION) + }) + env.ComponentVersion(COMP2, VERSION, func() { + env.Reference("ref", COMP4, VERSION) + }) + env.ComponentVersion(COMP3, VERSION, func() { + env.Reference("ref", COMP4, VERSION) + }) + env.ComponentVersion(COMP4, VERSION, func() { + }) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/x v1 OK +`)) + }) + It("handles all", func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMP, VERSION, func() { + env.Reference("ref1", COMP2, VERSION) + env.Reference("ref2", COMP3, VERSION) + }) + env.ComponentVersion(COMP2, VERSION, func() { + env.Reference("ref", COMP4, VERSION) + }) + env.ComponentVersion(COMP3, VERSION, func() { + env.Reference("ref", COMP4, VERSION) + }) + env.ComponentVersion(COMP4, VERSION, func() { + }) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/a v1 OK +test.de/x v1 OK +test.de/y v1 OK +test.de/z v1 OK +`)) + }) + + It("finds cycle", func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMP, VERSION, func() { + env.Reference("ref", COMP3, VERSION) + }) + env.ComponentVersion(COMP2, VERSION, func() { + env.Reference("ref", COMP3, VERSION) + }) + env.ComponentVersion(COMP3, VERSION, func() { + env.Reference("ref", COMP2, VERSION) + }) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/x v1 Error component version recursion: use of test.de/z:v1 for test.de/x:v1->test.de/z:v1->test.de/y:v1 +`)) + }) + + It("finds all cycles", func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMP, VERSION, func() { + env.Reference("ref", COMP2, VERSION) + env.Reference("ref", COMP3, VERSION) + }) + env.ComponentVersion(COMP2, VERSION, func() { + env.Reference("ref", COMP4, VERSION) + }) + env.ComponentVersion(COMP3, VERSION, func() { + env.Reference("ref", COMP4, VERSION) + }) + env.ComponentVersion(COMP4, VERSION, func() { + env.Reference("ref", COMP, VERSION) + }) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/a v1 Error component version recursion: use of test.de/a:v1 for test.de/a:v1->test.de/x:v1->test.de/z:v1 +test.de/x v1 Error component version recursion: use of test.de/x:v1 for test.de/x:v1->test.de/z:v1->test.de/a:v1 +test.de/y v1 Error component version recursion: use of test.de/a:v1 for test.de/y:v1->test.de/a:v1->test.de/x:v1->test.de/z:v1 +test.de/z v1 Error component version recursion: use of test.de/z:v1 for test.de/z:v1->test.de/a:v1->test.de/x:v1 +`)) + }) + + Context("finds non-local resources", func() { + BeforeEach(func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMP, VERSION, func() { + env.Resource("rsc1", VERSION, resourcetypes.BLUEPRINT, v1.LocalRelation, func() { + env.ModificationOptions(ocm.SkipDigest()) + env.Access(ociartifact.New("bla")) + }) + }) + }) + }) + + It("outputs table", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP, "--local-resources")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR +test.de/x v1 Resources +`)) + }) + + It("outputs wide table", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("check", "components", ARCH+"//"+COMP, "--local-resources", "-o=wide")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +COMPONENT VERSION STATUS ERROR MISSING NON-LOCAL +test.de/x v1 Resources RSC("name"="rsc1") +`)) + }) + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/components/check/options.go b/cmds/ocm/commands/ocmcmds/components/check/options.go new file mode 100644 index 0000000000..2278115f62 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/check/options.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "github.com/spf13/pflag" + + "github.com/open-component-model/ocm/cmds/ocm/pkg/options" +) + +func From(o options.OptionSetProvider) *Option { + var opt *Option + o.AsOptionSet().Get(&opt) + return opt +} + +var _ options.Options = (*Option)(nil) + +type Option struct { + CheckLocalResources bool + CheckLocalSources bool +} + +func NewOption() *Option { + return &Option{} +} + +func (o *Option) AddFlags(fs *pflag.FlagSet) { + fs.BoolVarP(&o.CheckLocalResources, "local-resources", "R", false, "check also for describing resources with local access method, only") + fs.BoolVarP(&o.CheckLocalSources, "local-sources", "S", false, "check also for describing sources with local access method, only") +} + +func (o *Option) Usage() string { + s := ` +If the options --local-resources and/or --local-sources are given the +the check additionally assures that all resources or sources are included into the component version. +This means that they are using local access methods, only. +` + return s +} diff --git a/cmds/ocm/commands/ocmcmds/components/check/suite_test.go b/cmds/ocm/commands/ocmcmds/components/check/suite_test.go new file mode 100644 index 0000000000..c7753f5d8d --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/check/suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package check_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OCM check components") +} diff --git a/cmds/ocm/commands/ocmcmds/components/cmd.go b/cmds/ocm/commands/ocmcmds/components/cmd.go index ad7ddf1f1d..7bee1e2726 100644 --- a/cmds/ocm/commands/ocmcmds/components/cmd.go +++ b/cmds/ocm/commands/ocmcmds/components/cmd.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/components/add" + "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/components/check" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/components/download" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/components/get" "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/components/hash" @@ -38,4 +39,5 @@ func AddCommands(ctx clictx.Context, cmd *cobra.Command) { cmd.AddCommand(transfer.NewCommand(ctx, transfer.Verb)) cmd.AddCommand(verify.NewCommand(ctx, verify.Verb)) cmd.AddCommand(download.NewCommand(ctx, download.Verb)) + cmd.AddCommand(check.NewCommand(ctx, check.Verb)) } diff --git a/cmds/ocm/commands/verbs/add/cmd.go b/cmds/ocm/commands/verbs/add/cmd.go index 1e9dd36303..1a1463eded 100644 --- a/cmds/ocm/commands/verbs/add/cmd.go +++ b/cmds/ocm/commands/verbs/add/cmd.go @@ -22,7 +22,7 @@ import ( // NewCommand creates a new command. func NewCommand(ctx clictx.Context) *cobra.Command { cmd := utils.MassageCommand(&cobra.Command{ - Short: "Add resources or sources to a component archive", + Short: "Add elements to a component repository or component version", }, verbs.Add) cmd.AddCommand(resourceconfig.NewCommand(ctx)) cmd.AddCommand(sourceconfig.NewCommand(ctx)) diff --git a/cmds/ocm/commands/verbs/check/cmd.go b/cmds/ocm/commands/verbs/check/cmd.go new file mode 100644 index 0000000000..b88bb2fcab --- /dev/null +++ b/cmds/ocm/commands/verbs/check/cmd.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" + "github.com/open-component-model/ocm/cmds/ocm/pkg/utils" + "github.com/open-component-model/ocm/pkg/contexts/clictx" + "github.com/spf13/cobra" + + components "github.com/open-component-model/ocm/cmds/ocm/commands/ocmcmds/components/check" +) + +// NewCommand creates a new command. +func NewCommand(ctx clictx.Context) *cobra.Command { + cmd := utils.MassageCommand(&cobra.Command{ + Short: "check components in OCM repository", + }, verbs.Check) + cmd.AddCommand(components.NewCommand(ctx)) + return cmd +} diff --git a/cmds/ocm/commands/verbs/verbs.go b/cmds/ocm/commands/verbs/verbs.go index 20be5eb33f..b7ed57982f 100644 --- a/cmds/ocm/commands/verbs/verbs.go +++ b/cmds/ocm/commands/verbs/verbs.go @@ -6,6 +6,7 @@ package verbs const ( Get = "get" + Check = "check" Describe = "describe" Hash = "hash" Add = "add" diff --git a/docs/reference/ocm.md b/docs/reference/ocm.md index 9a4ebd6349..d6aa943a85 100644 --- a/docs/reference/ocm.md +++ b/docs/reference/ocm.md @@ -11,7 +11,7 @@ ocm [] ... ``` -X, --attribute stringArray attribute setting --ca-cert stringArray additional root certificate authorities - --config string configuration file + --config stringArray configuration file --config-set strings apply configuration set -C, --cred stringArray credential setting -h, --help help for ocm @@ -225,6 +225,28 @@ 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* + + General root certificate settings given as JSON document with the following + format: + +
+  {
+    "rootCertificates"": [
+       {
+         "data": ""<base64>"
+       },
+       {
+         "path": ""<file path>"
+       }
+    ],
+  
+ + One of following data fields are possible: + - data: base64 encoded binary data + - stringdata: plain text data + - path: a file path to read the data from + - github.com/mandelsoft/ocm/signing: *JSON* Public and private Key settings given as JSON document with the following @@ -318,8 +340,9 @@ by a certificate delivered with the signature. ##### Sub Commands -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm bootstrap](ocm_bootstrap.md) — bootstrap components +* [ocm check](ocm_check.md) — check components in OCM repository * [ocm clean](ocm_clean.md) — Cleanup/re-organize elements * [ocm controller](ocm_controller.md) — Commands acting on the ocm-controller * [ocm create](ocm_create.md) — Create transport or component archive diff --git a/docs/reference/ocm_add.md b/docs/reference/ocm_add.md index d1a191066d..c479623e85 100644 --- a/docs/reference/ocm_add.md +++ b/docs/reference/ocm_add.md @@ -1,4 +1,4 @@ -## ocm add — Add Resources Or Sources To A Component Archive +## ocm add — Add Elements To A Component Repository Or Component Version ### Synopsis diff --git a/docs/reference/ocm_add_componentversions.md b/docs/reference/ocm_add_componentversions.md index 0344902567..66c18b99fb 100644 --- a/docs/reference/ocm_add_componentversions.md +++ b/docs/reference/ocm_add_componentversions.md @@ -191,7 +191,7 @@ next to the description file. ##### Parents -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_add_componentversions_ocm-labels.md b/docs/reference/ocm_add_componentversions_ocm-labels.md index c4a3c1291c..13f59fcf92 100644 --- a/docs/reference/ocm_add_componentversions_ocm-labels.md +++ b/docs/reference/ocm_add_componentversions_ocm-labels.md @@ -177,6 +177,6 @@ The following label assignments are configured: ##### Parents * [ocm add componentversions](ocm_add_componentversions.md) — add component version(s) to a (new) transport archive -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_add_references.md b/docs/reference/ocm_add_references.md index 57445da8f2..d03ae06f3f 100644 --- a/docs/reference/ocm_add_references.md +++ b/docs/reference/ocm_add_references.md @@ -183,6 +183,6 @@ $ ocm add references path/to/ca references.yaml VERSION=1.0.0 ##### Parents -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_add_resource-configuration.md b/docs/reference/ocm_add_resource-configuration.md index 859e884c53..bd9c75f606 100644 --- a/docs/reference/ocm_add_resource-configuration.md +++ b/docs/reference/ocm_add_resource-configuration.md @@ -809,7 +809,7 @@ $ ocm add resource-config resources.yaml --name myresource --type PlainText --in ##### Parents -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_add_resources.md b/docs/reference/ocm_add_resources.md index 7c4fabd9b1..f317599b57 100644 --- a/docs/reference/ocm_add_resources.md +++ b/docs/reference/ocm_add_resources.md @@ -846,7 +846,7 @@ $ ocm add resources ‐‐file path/to/ca resources.yaml VERSION=1.0.0 ##### Parents -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_add_routingslips.md b/docs/reference/ocm_add_routingslips.md index 4f07335612..ce8478ffa4 100644 --- a/docs/reference/ocm_add_routingslips.md +++ b/docs/reference/ocm_add_routingslips.md @@ -125,6 +125,6 @@ $ ocm add routingslip ghcr.io/mandelsoft/ocm//ocmdemoinstaller:0.0.1-dev mandels ##### Parents -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_add_source-configuration.md b/docs/reference/ocm_add_source-configuration.md index 3fa3538579..a4a3cd8fbc 100644 --- a/docs/reference/ocm_add_source-configuration.md +++ b/docs/reference/ocm_add_source-configuration.md @@ -809,7 +809,7 @@ $ ocm add source-config sources.yaml --name sources --type filesystem --access ' ##### Parents -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_add_sources.md b/docs/reference/ocm_add_sources.md index d448a9d15a..205e1ebb68 100644 --- a/docs/reference/ocm_add_sources.md +++ b/docs/reference/ocm_add_sources.md @@ -816,7 +816,7 @@ $ ocm add sources --file path/to/cafile sources.yaml ##### Parents -* [ocm add](ocm_add.md) — Add resources or sources to a component archive +* [ocm add](ocm_add.md) — Add elements to a component repository or component version * [ocm](ocm.md) — Open Component Model command line client diff --git a/docs/reference/ocm_attributes.md b/docs/reference/ocm_attributes.md index 772c17f214..2e18ff693f 100644 --- a/docs/reference/ocm_attributes.md +++ b/docs/reference/ocm_attributes.md @@ -133,6 +133,28 @@ OCM library: Directory to look for OCM plugin executables. +- github.com/mandelsoft/ocm/rootcerts: *JSON* + + General root certificate settings given as JSON document with the following + format: + +
+  {
+    "rootCertificates"": [
+       {
+         "data": ""<base64>"
+       },
+       {
+         "path": ""<file path>"
+       }
+    ],
+  
+ + One of following data fields are possible: + - data: base64 encoded binary data + - stringdata: plain text data + - path: a file path to read the data from + - github.com/mandelsoft/ocm/signing: *JSON* Public and private Key settings given as JSON document with the following diff --git a/docs/reference/ocm_check.md b/docs/reference/ocm_check.md new file mode 100644 index 0000000000..0d3a7b9f2a --- /dev/null +++ b/docs/reference/ocm_check.md @@ -0,0 +1,25 @@ +## ocm check — Check Components In OCM Repository + +### Synopsis + +``` +ocm check [] ... +``` + +### Options + +``` + -h, --help help for check +``` + +### SEE ALSO + +##### Parents + +* [ocm](ocm.md) — Open Component Model command line client + + +##### Sub Commands + +* [ocm check componentversions](ocm_check_componentversions.md) — Check completeness of a component version in an OCM repository + diff --git a/docs/reference/ocm_check_componentversions.md b/docs/reference/ocm_check_componentversions.md new file mode 100644 index 0000000000..66af32da98 --- /dev/null +++ b/docs/reference/ocm_check_componentversions.md @@ -0,0 +1,104 @@ +## ocm check componentversions — Check Completeness Of A Component Version In An OCM Repository + +### Synopsis + +``` +ocm check componentversions [] {} +``` + +##### Aliases + +``` +componentversions, componentversion, cv, components, component, comps, comp, c +``` + +### Options + +``` + --fail-on-error fail on validation error + -h, --help help for componentversions + -R, --local-resources check also for describing resources with local access method, only + -S, --local-sources check also for describing sources with local access method, only + -o, --output string output mode (JSON, json, wide, yaml) + --repo string repository name or spec + -s, --sort stringArray sort fields +``` + +### Description + + +This command checks, whether component versions are completely contained +in an OCM repository with all its dependent component references. + + +If the --repo option is specified, the given names are interpreted +relative to the specified repository using the syntax + +
+
<component>[:<version>]
+
+ +If no --repo option is specified the given names are interpreted +as located OCM component version references: + +
+
[<repo type>::]<host>[:<port>][/<base path>]//<component>[:<version>]
+
+ +Additionally there is a variant to denote common transport archives +and general repository specifications + +
+
[<repo type>::]<filepath>|<spec json>[//<component>[:<version>]]
+
+ +The --repo option takes an OCM repository specification: + +
+
[<repo type>::]<configured name>|<file path>|<spec json>
+
+ +For the *Common Transport Format* the types directory, +tar or tgz is possible. + +Using the JSON variant any repository types supported by the +linked library can be used: + +Dedicated OCM repository types: + - ComponentArchive: v1 + +OCI Repository types (using standard component repository to OCI mapping): + - CommonTransportFormat: v1 + - OCIRegistry: v1 + - oci: v1 + - ociRegistry + + + +If the options --local-resources and/or --local-sources are given the +the check additionally assures that all resources or sources are included into the component version. +This means that they are using local access methods, only. + +With the option --output the output mode can be selected. +The following modes are supported: + - (default) + - JSON + - json + - wide + - yaml + + +### Examples + +``` +$ ocm check componentversion ghcr.io/mandelsoft/kubelink +$ ocm get componentversion --repo OCIRegistry::ghcr.io mandelsoft/kubelink +``` + +### SEE ALSO + +##### Parents + +* [ocm check](ocm_check.md) — check components in OCM repository +* [ocm](ocm.md) — Open Component Model command line client + diff --git a/docs/reference/ocm_configfile.md b/docs/reference/ocm_configfile.md index 8978d8b79f..2265aebee5 100644 --- a/docs/reference/ocm_configfile.md +++ b/docs/reference/ocm_configfile.md @@ -264,6 +264,17 @@ The following configuration types are supported: config: <arbitrary configuration structure> disableAutoRegistration: <boolean flag to disable auto registration for up- and download handlers> +- rootcerts.config.ocm.software + The config type rootcerts.config.ocm.software can be used to define + general root certificates. A certificate value might be given by one of the fields: + - path: path of file with key data + - data: base64 encoded binary data + - stringdata: data a string parsed by key handler + +
+      rootCertificates:
+        - path: <file path>
+  
- scripts.ocm.config.ocm.software The config type scripts.ocm.config.ocm.software can be used to define transfer scripts: diff --git a/docs/reference/ocm_get_credentials.md b/docs/reference/ocm_get_credentials.md index f79744183f..8d56179552 100644 --- a/docs/reference/ocm_get_credentials.md +++ b/docs/reference/ocm_get_credentials.md @@ -17,6 +17,7 @@ credentials, creds, cred ``` -h, --help help for credentials -m, --matcher string matcher type override + -s, --sloppy sloppy matching of consumer type ``` ### Description diff --git a/docs/reference/ocm_get_routingslips.md b/docs/reference/ocm_get_routingslips.md index 2b856fc63f..518d9d3c0c 100644 --- a/docs/reference/ocm_get_routingslips.md +++ b/docs/reference/ocm_get_routingslips.md @@ -17,7 +17,7 @@ routingslips, routingslip, rs ``` --all-columns show all table columns -c, --constraints constraints version constraint - --fail-on-error fail on label validation error + --fail-on-error fail on validation error -h, --help help for routingslips --latest restrict component versions to latest --lookup stringArray repository name or spec for closure lookup fallback diff --git a/docs/reference/ocm_logging.md b/docs/reference/ocm_logging.md index e44918e11e..78950872ce 100644 --- a/docs/reference/ocm_logging.md +++ b/docs/reference/ocm_logging.md @@ -19,6 +19,7 @@ The following *realms* are used by the command line tool: - ocm: general realm used for the ocm go library. - ocm/accessmethod/ociartifact: access method ociArtifact - ocm/compdesc: component descriptor handling + - ocm/config: configuration management - ocm/context: context lifecycle - ocm/credentials/dockerconfig: docker config handling as credential repository - ocm/credentials/vault: HashiCorp Vault Access diff --git a/go.mod b/go.mod index ae4463a997..40d80245c1 100644 --- a/go.mod +++ b/go.mod @@ -84,6 +84,7 @@ require ( github.com/distribution/reference v0.5.0 github.com/imdario/mergo v0.3.16 github.com/mandelsoft/vfs v0.4.0 + github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c k8s.io/client-go v0.28.4 ) diff --git a/go.sum b/go.sum index f7a2523273..3c74c6bc05 100644 --- a/go.sum +++ b/go.sum @@ -2279,6 +2279,8 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c h1:HelZ2kAFadG0La9d+4htN4HzQ68Bm2iM9qKMSMES6xg= +github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c/go.mod h1:JlzghshsemAMDGZLytTFY8C1JQxQPhnatWqNwUXjggo= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/theupdateframework/go-tuf v0.6.1 h1:6J89fGjQf7s0mLmTG7p7pO/MbKOg+bIXhaLyQdmbKuE= diff --git a/pkg/common/history.go b/pkg/common/history.go index f75b7b16d7..339563381a 100644 --- a/pkg/common/history.go +++ b/pkg/common/history.go @@ -117,6 +117,15 @@ func (h History) Compare2(o History) (int, bool) { return len(h) - len(o), false } +func (h History) Cycle(nv NameVersion) (History, History) { + for i, e := range h { + if e == nv { + return h[:i], h[i:].Append(nv) + } + } + return nil, nil +} + //////////////////////////////////////////////////////////////////////////////// type HistoryElement interface { diff --git a/pkg/common/types.go b/pkg/common/types.go index 8946dbb092..5468a2dee9 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -25,7 +25,10 @@ type NameVersion struct { version string } -var _ VersionedElement = (*NameVersion)(nil) +var ( + _ json.Marshaler = (*NameVersion)(nil) + _ VersionedElement = (*NameVersion)(nil) +) func NewNameVersion(name, version string) NameVersion { return NameVersion{name, version} diff --git a/pkg/contexts/credentials/utils.go b/pkg/contexts/credentials/utils.go index 8b8d9fb884..725714a0a6 100644 --- a/pkg/contexts/credentials/utils.go +++ b/pkg/contexts/credentials/utils.go @@ -5,6 +5,10 @@ package credentials import ( + "strings" + + "github.com/texttheater/golang-levenshtein/levenshtein" + "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/utils" ) @@ -28,3 +32,63 @@ func CredentialsFor(ctx ContextProvider, obj interface{}, uctx ...UsageContext) } return CredentialsForConsumer(ctx, id) } + +func GuessConsumerType(ctxp ContextProvider, spec string) string { + matchers := ctxp.CredentialsContext().ConsumerIdentityMatchers() + lspec := strings.ToLower(spec) + + if matchers.Get(spec) == nil { + fix := "" + for _, i := range matchers.List() { + idx := strings.Index(i.Type, ".") + if idx > 0 && i.Type[:idx] == spec { + fix = i.Type + break + } + } + if fix == "" { + for _, i := range matchers.List() { + if strings.ToLower(i.Type) == lspec { + fix = i.Type + break + } + } + } + if fix == "" { + for _, i := range matchers.List() { + idx := strings.Index(i.Type, ".") + if idx > 0 && strings.ToLower(i.Type[:idx]) == lspec { + fix = i.Type + break + } + } + } + if fix == "" { + min := -1 + for _, i := range matchers.List() { + idx := strings.Index(i.Type, ".") + if idx > 0 { + d := levenshtein.DistanceForStrings([]rune(lspec), []rune(strings.ToLower(i.Type[:idx])), levenshtein.DefaultOptions) + if d < 5 && fix == "" || min > d { + fix = i.Type + min = d + } + } + } + } + if fix == "" { + min := -1 + for _, i := range matchers.List() { + d := levenshtein.DistanceForStrings([]rune(lspec), []rune(strings.ToLower(i.Type)), levenshtein.DefaultOptions) + if d < 5 && fix == "" || min > d { + fix = i.Type + min = d + } + } + } + if fix != "" { + return fix + } + } + return spec +} diff --git a/pkg/errors/errprop.go b/pkg/errors/errprop.go index 0518e36b2b..b1a3a2047f 100644 --- a/pkg/errors/errprop.go +++ b/pkg/errors/errprop.go @@ -4,6 +4,10 @@ package errors +import ( + "github.com/mandelsoft/logging" +) + type ErrorFunction func() error // PropagateError propagates a deferred error to the named return value @@ -22,3 +26,10 @@ func PropagateErrorf(errp *error, f ErrorFunction, msg string, args ...interface *errp = ErrListf(msg, args...).Add(*errp, f()).Result() } } + +func LogError(log logging.Logger, f ErrorFunction, msg string, keypair ...interface{}) { + err := f() + if err != nil { + log.LogError(err, msg, keypair...) + } +}