From 8528e4eb8a5b3138d6c3091f3a07df2cb88efa10 Mon Sep 17 00:00:00 2001 From: Fabian Holler Date: Wed, 12 Jun 2024 16:27:40 +0200 Subject: [PATCH] add "dependencies-tool contains" sub-command Add the sub-command: dependencies-tool contains ROOT-DIR|DEP-TREE-FILE DISTRIBUTION APP The command checks if a given distribution contains an app with the given name. If it does it exits with code 0, otherwise with code 2. Additionally a result message is printed to stdout. --- internal/cmd/contains.go | 82 +++++++++++++++++++++++++++++++ internal/cmd/errors.go | 27 ++++++++++ internal/cmd/exitcodes.go | 7 +++ internal/cmd/order.go | 17 +------ internal/cmd/root.go | 31 ++++++++++-- internal/deps/composition.go | 12 +++++ internal/deps/composition_test.go | 19 +++++++ 7 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 internal/cmd/contains.go create mode 100644 internal/cmd/errors.go create mode 100644 internal/cmd/exitcodes.go diff --git a/internal/cmd/contains.go b/internal/cmd/contains.go new file mode 100644 index 0000000..617b7fb --- /dev/null +++ b/internal/cmd/contains.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/simplesurance/dependencies-tool/v3/internal/cmd/fs" + + "github.com/spf13/cobra" +) + +var containsLongHelp = fmt.Sprintf(` +Check if an app is part of a distribution. + +Exit Codes: + %d - Success, app is part of the distribution + %d - Error + %d - App is not part of the distribution +`, ExitCodeSuccess, ExitCodeError, ExitCodeNotFound) + +type containsCmd struct { + root *rootCmd + *cobra.Command + + src string + distribution string + app string + srcType fs.PathType +} + +func newContainsCmd(root *rootCmd) *containsCmd { + cmd := containsCmd{ + Command: &cobra.Command{ + Use: "contains ROOT-DIR|DEP-TREE-FILE DISTRIBUTION APP", + Short: "Check if an app is part of a distribution", + Args: cobra.ExactArgs(3), + Long: strings.TrimSpace(containsLongHelp), + }, + root: root, + } + + cmd.PreRunE = func(_ *cobra.Command, args []string) error { + cmd.src = args[0] + cmd.distribution = args[1] + cmd.app = args[2] + + pType, err := fs.FileOrDir(args[0]) + if err != nil { + return err + } + + cmd.srcType = pType + + return nil + } + cmd.RunE = cmd.run + + return &cmd +} + +func (c *containsCmd) run(*cobra.Command, []string) error { + composition, err := c.root.loadComposition(c.srcType, c.src) + if err != nil { + return err + } + exists, err := composition.Contains(c.distribution, c.app) + if err != nil { + return fmt.Errorf("checking if %q is part of %q failed: %w", c.app, c.distribution, err) + } + + if exists { + fmt.Printf("%q is part of the distribution %q\n", c.app, c.distribution) + return nil + } + + fmt.Printf("%q is not part of the distribution %q\n", c.app, c.distribution) + + // do not print the error, result message has already been printed to + // stdout + c.SilenceErrors = true + return NewErrWithExitCode(nil, ExitCodeNotFound) +} diff --git a/internal/cmd/errors.go b/internal/cmd/errors.go new file mode 100644 index 0000000..290704e --- /dev/null +++ b/internal/cmd/errors.go @@ -0,0 +1,27 @@ +package cmd + +import "fmt" + +type ErrWithExitCode struct { + exitCode int + err error +} + +func NewErrWithExitCode(originalError error, exitCode int) *ErrWithExitCode { + return &ErrWithExitCode{ + exitCode: exitCode, + err: originalError, + } +} + +func (e *ErrWithExitCode) Unwrap() error { + return e.err +} + +func (e *ErrWithExitCode) Error() string { + if e.err == nil { + return fmt.Sprintf("ErrWithExitCode: %d", e.exitCode) + } + + return e.err.Error() +} diff --git a/internal/cmd/exitcodes.go b/internal/cmd/exitcodes.go new file mode 100644 index 0000000..ab8ea24 --- /dev/null +++ b/internal/cmd/exitcodes.go @@ -0,0 +1,7 @@ +package cmd + +const ( + ExitCodeSuccess = 0 + ExitCodeError = 1 + ExitCodeNotFound = 2 +) diff --git a/internal/cmd/order.go b/internal/cmd/order.go index 0ecf5ec..b4b4a77 100644 --- a/internal/cmd/order.go +++ b/internal/cmd/order.go @@ -9,7 +9,6 @@ import ( "github.com/spf13/cobra" "github.com/simplesurance/dependencies-tool/v3/internal/cmd/fs" - "github.com/simplesurance/dependencies-tool/v3/internal/deps" ) const orderShortHelp = "Generate a deployment order." @@ -92,7 +91,7 @@ func newOrderCmd(root *rootCmd) *orderCmd { } func (c *orderCmd) run(cc *cobra.Command, _ []string) error { - composition, err := c.load() + composition, err := c.root.loadComposition(c.srcType, c.src) if err != nil { return err } @@ -132,17 +131,3 @@ func validateAppsParam(apps []string) error { } return nil } - -func (c *orderCmd) load() (*deps.Composition, error) { - switch c.srcType { - case fs.PathTypeDir: - return deps.CompositionFromDir(c.src, c.root.cfgName, c.root.ignoredDirs) - - case fs.PathTypeFile: - return deps.CompositionFromJSON(c.src) - - default: - panic(fmt.Sprintf("SrcType has unexpected value: %d", c.srcType)) - } - -} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 2b4f003..c762355 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -1,9 +1,14 @@ package cmd import ( + "errors" + "fmt" "os" "path/filepath" + "github.com/simplesurance/dependencies-tool/v3/internal/cmd/fs" + "github.com/simplesurance/dependencies-tool/v3/internal/deps" + "github.com/spf13/cobra" ) @@ -48,19 +53,39 @@ func newRoot() *rootCmd { defaultExcludeDirs, "comma-separated list of directory names that are excluded when searching for configuration files", ) + + r.AddCommand(newContainsCmd(&r).Command) + r.AddCommand(newExportCmd(&r).Command) r.AddCommand(newOrderCmd(&r).Command) r.AddCommand(newVerify(&r).Command) - r.AddCommand(newExportCmd(&r).Command) return &r } +func (r *rootCmd) loadComposition(srcType fs.PathType, src string) (*deps.Composition, error) { + switch srcType { + case fs.PathTypeDir: + return deps.CompositionFromDir(src, r.cfgName, r.ignoredDirs) + + case fs.PathTypeFile: + return deps.CompositionFromJSON(src) + + default: + panic(fmt.Sprintf("SrcType has unexpected value: %d", srcType)) + } +} + func Execute() { cmd := newRoot() cmd.SetOut(os.Stdout) if err := cmd.Execute(); err != nil { - os.Exit(1) + var ee *ErrWithExitCode + if errors.As(err, &ee) { + os.Exit(ee.exitCode) + } + + os.Exit(ExitCodeError) } - os.Exit(0) + os.Exit(ExitCodeSuccess) } diff --git a/internal/deps/composition.go b/internal/deps/composition.go index 5a63de4..4ba57c3 100644 --- a/internal/deps/composition.go +++ b/internal/deps/composition.go @@ -139,6 +139,18 @@ func (c *Composition) Add(distribution, appName string, app *Dependencies) { distr[appName] = app } +// Contains returns true if the distribution contains appName, otherwise false. +// If the distribution does not exist an error is returned. +func (c *Composition) Contains(distribution, appName string) (bool, error) { + distr, exists := c.Distribution[distribution] + if !exists { + return false, errors.New("distribution does not exist") + } + + _, exists = distr[appName] + return exists, nil +} + func (c *Composition) ToJSONFile(path string) error { f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) if err != nil { diff --git a/internal/deps/composition_test.go b/internal/deps/composition_test.go index 39c9c64..4126c52 100644 --- a/internal/deps/composition_test.go +++ b/internal/deps/composition_test.go @@ -18,6 +18,25 @@ func TestDeploymentOrderNoDeps(t *testing.T) { require.ElementsMatch(t, []string{"app1", "app2"}, order) } +func TestCompositionContains(t *testing.T) { + comp := NewComposition() + comp.Add("prd", "app1", &Dependencies{}) + comp.Add("stg", "app1", &Dependencies{}) + comp.Add("prd", "appX", &Dependencies{}) + + exists, err := comp.Contains("abc", "app1") + require.Error(t, err) + assert.False(t, exists) + + exists, err = comp.Contains("prd", "app1") + require.NoError(t, err) + assert.True(t, exists) + + exists, err = comp.Contains("prd", "app2") + require.NoError(t, err) + assert.False(t, exists) +} + func TestDeploymentOrder(t *testing.T) { /* Dependency structure: