From 680e782a072c4174ddd1f25837488a49d5c7803f Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 2 Feb 2024 18:08:02 -0800 Subject: [PATCH 01/22] Add safecli dependency --- go.mod | 2 ++ go.sum | 2 ++ 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index bb49e9f3df..0b81ec2854 100644 --- a/go.mod +++ b/go.mod @@ -216,6 +216,8 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) +require github.com/kanisterio/safecli v0.0.3 + require ( github.com/Azure/go-autorest/autorest v0.11.27 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect diff --git a/go.sum b/go.sum index 9793c1f653..fed0f3304c 100644 --- a/go.sum +++ b/go.sum @@ -359,6 +359,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kanisterio/safecli v0.0.3 h1:ts3oRVSoRexFBv9pOdWYZsN4k068pWx5Cl4zN54mQro= +github.com/kanisterio/safecli v0.0.3/go.mod h1:fK3Mcbeiso+NtkUdhGugK0Vf4S2l8ObvFf564ry1W5A= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734/go.mod h1:rdqSnvOJuKCPFW/h2rVLuXOAkRnHHdp9PZcKx4HCoDM= github.com/kastenhq/stow v0.2.6-kasten.1.0.20231101232131-9321daa23aae h1:2cl4yuAJpdmLCx7G8eIsfNlQBLEfw0JDj6mTTyqc5qg= From 72011e75bc8478d1856b1e86a8f6bee896f55635 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 2 Feb 2024 18:09:47 -0800 Subject: [PATCH 02/22] add new flag implementations based on the safecli package for the Kopia CLI --- pkg/kopia/cli/errors.go | 25 +++ pkg/kopia/cli/internal/flag/bool_flag.go | 45 ++++++ pkg/kopia/cli/internal/flag/flag.go | 81 ++++++++++ pkg/kopia/cli/internal/flag/flag_test.go | 171 +++++++++++++++++++++ pkg/kopia/cli/internal/flag/string_flag.go | 78 ++++++++++ pkg/kopia/cli/internal/test/flag_suite.go | 112 ++++++++++++++ 6 files changed, 512 insertions(+) create mode 100644 pkg/kopia/cli/errors.go create mode 100644 pkg/kopia/cli/internal/flag/bool_flag.go create mode 100644 pkg/kopia/cli/internal/flag/flag.go create mode 100644 pkg/kopia/cli/internal/flag/flag_test.go create mode 100644 pkg/kopia/cli/internal/flag/string_flag.go create mode 100644 pkg/kopia/cli/internal/test/flag_suite.go diff --git a/pkg/kopia/cli/errors.go b/pkg/kopia/cli/errors.go new file mode 100644 index 0000000000..946dd04e87 --- /dev/null +++ b/pkg/kopia/cli/errors.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "github.com/pkg/errors" +) + +// flag errors +var ( + // ErrInvalidFlag is returned when the flag name is empty. + ErrInvalidFlag = errors.New("invalid flag") +) diff --git a/pkg/kopia/cli/internal/flag/bool_flag.go b/pkg/kopia/cli/internal/flag/bool_flag.go new file mode 100644 index 0000000000..4eea665cb2 --- /dev/null +++ b/pkg/kopia/cli/internal/flag/bool_flag.go @@ -0,0 +1,45 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag + +import ( + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli" +) + +// boolFlag defines a boolean flag with a given flag name. +// If enabled is set to true, the flag is applied; otherwise, it is not. +type boolFlag struct { + flag string + enabled bool +} + +// Apply appends the flag to the command if the flag is enabled. +func (f boolFlag) Apply(cli safecli.CommandAppender) error { + if f.enabled { + cli.AppendLoggable(f.flag) + } + return nil +} + +// NewBoolFlag creates a new bool flag with a given flag name. +// If the flag name is empty, cli.ErrInvalidFlag is returned. +func NewBoolFlag(flag string, enabled bool) Applier { + if flag == "" { + return ErrorFlag(cli.ErrInvalidFlag) + } + return boolFlag{flag, enabled} +} diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go new file mode 100644 index 0000000000..898ae31e16 --- /dev/null +++ b/pkg/kopia/cli/internal/flag/flag.go @@ -0,0 +1,81 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag + +import ( + "github.com/kanisterio/safecli" +) + +// Applier applies flags/args to the command. +type Applier interface { + // Apply applies the flags/args to the command. + Apply(cli safecli.CommandAppender) error +} + +// Apply appends multiple flags to the CLI. +// If any of the flags encounter an error during the Apply process, +// the error is returned and no changes are made to the CLI. +// If no error, the flags are appended to the CLI. +func Apply(cli safecli.CommandAppender, flags ...Applier) error { + // create a new sub builder which will be used to apply the flags + // to avoid mutating the CLI if an error is encountered. + sub := safecli.NewBuilder() + for _, flag := range flags { + if flag == nil { // if the flag is nil, skip it + continue + } + if err := flag.Apply(cli); err != nil { + return err + } + } + cli.Append(sub) + return nil +} + +// flags defines a collection of Flags. +type flags []Applier + +// Apply applies the flags to the CLI. +func (flags flags) Apply(cli safecli.CommandAppender) error { + return Apply(cli, flags...) +} + +// NewFlags creates a new collection of flags. +func NewFlags(fs ...Applier) Applier { + return flags(fs) +} + +// simpleFlag is a simple implementation of the Applier interface. +type simpleFlag struct { + err error +} + +// Apply does nothing except return an error if one is set. +func (f simpleFlag) Apply(safecli.CommandAppender) error { + return f.err +} + +// EmptyFlag creates a new flag that does nothing. +// It is useful for creating a no-op flag when a condition is not met +// but Applier interface is required. +func EmptyFlag() Applier { + return simpleFlag{} +} + +// ErrorFlag creates a new flag that returns an error when applied. +// It is useful for creating a flag validation if a condition is not met. +func ErrorFlag(err error) Applier { + return simpleFlag{err} +} diff --git a/pkg/kopia/cli/internal/flag/flag_test.go b/pkg/kopia/cli/internal/flag/flag_test.go new file mode 100644 index 0000000000..c20a46b450 --- /dev/null +++ b/pkg/kopia/cli/internal/flag/flag_test.go @@ -0,0 +1,171 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag_test + +import ( + "errors" + "testing" + + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/test" +) + +var ( + ErrFlag = errors.New("flag error") +) + +// MockFlagApplier is a mock implementation of the FlagApplier interface. +type MockFlagApplier struct { + flagName string + applyErr error +} + +func (m *MockFlagApplier) Apply(cli safecli.CommandAppender) error { + cli.AppendLoggable(m.flagName) + return m.applyErr +} + +func TestApply(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.FlagSuite{Cmd: "cmd", Tests: []test.FlagTest{ + { + Name: "Apply with no flags should generate only the command", + ExpectedCLI: []string{"cmd"}, + }, + { + Name: "Apply with nil flags should generate only the command", + Flag: flag.NewFlags(nil, nil), + ExpectedCLI: []string{"cmd"}, + }, + { + Name: "Apply with flags should generate the command and flags", + Flag: flag.NewFlags( + &MockFlagApplier{flagName: "--flag1", applyErr: nil}, + &MockFlagApplier{flagName: "--flag2", applyErr: nil}, + ), + ExpectedCLI: []string{"cmd", "--flag1", "--flag2"}, + }, + { + Name: "Apply with one error flag should not modify the command and return the error", + Flag: flag.NewFlags( + &MockFlagApplier{flagName: "flag1", applyErr: nil}, + &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: ErrFlag, + }, + { + Name: "NewBoolFlag", + Flag: flag.NewFlags( + flag.NewBoolFlag("--flag1", true), + flag.NewBoolFlag("--flag2", false), + ), + ExpectedCLI: []string{"cmd", "--flag1"}, + }, + { + Name: "NewBoolFlag with empty flag name should return an error", + Flag: flag.NewFlags( + flag.NewBoolFlag("", true), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewStringFlag", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + flag.NewStringFlag("--flag2", ""), + ), + ExpectedCLI: []string{"cmd", "--flag1=value1"}, + }, + { + Name: "NewStringFlag with all empty values should return an error", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + flag.NewStringFlag("", ""), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewRedactedStringFlag", + Flag: flag.NewFlags( + flag.NewRedactedStringFlag("--flag1", "value1"), + flag.NewRedactedStringFlag("--flag2", ""), + flag.NewRedactedStringFlag("", "value3"), + ), + ExpectedCLI: []string{"cmd", "--flag1=value1", "value3"}, + ExpectedLog: "cmd --flag1=<****> <****>", + }, + { + Name: "NewRedactedStringFlag with all empty values should return an error", + Flag: flag.NewFlags( + flag.NewRedactedStringFlag("--flag1", "value1"), + flag.NewRedactedStringFlag("", ""), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewStringValue", + Flag: flag.NewFlags( + flag.NewStringArgument("value1"), + ), + ExpectedCLI: []string{"cmd", "value1"}, + }, + { + Name: "NewStringValue with empty value should return an error", + Flag: flag.NewFlags( + flag.NewStringArgument(""), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewFlags should generate multiple flags", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + flag.NewRedactedStringFlag("--flag2", "value2"), + flag.NewStringArgument("value3"), + ), + ExpectedCLI: []string{"cmd", "--flag1=value1", "--flag2=value2", "value3"}, + ExpectedLog: "cmd --flag1=value1 --flag2=<****> value3", + }, + { + Name: "NewFlags should generate no flags if one of them returns an error", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: ErrFlag, + }, + { + Name: "EmptyFlag should not generate any flags", + Flag: flag.EmptyFlag(), + ExpectedCLI: []string{"cmd"}, + }, + { + Name: "ErrorFlag should return an error", + Flag: flag.ErrorFlag(ErrFlag), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: ErrFlag, + }, +}}) diff --git a/pkg/kopia/cli/internal/flag/string_flag.go b/pkg/kopia/cli/internal/flag/string_flag.go new file mode 100644 index 0000000000..a94c29722c --- /dev/null +++ b/pkg/kopia/cli/internal/flag/string_flag.go @@ -0,0 +1,78 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag + +import ( + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli" +) + +// stringFlag defines a string flag with a given flag name and value. +// If the value is empty, the flag is not applied. +type stringFlag struct { + flag string // flag name + value string // flag value + redacted bool // output the value as redacted +} + +// appenderFunc is a function that appends strings to a command. +type appenderFunc func(...string) *safecli.Builder + +// Apply appends the flag to the command if the value is not empty. +// If the value is redacted, it is appended as redacted. +func (f stringFlag) Apply(cli safecli.CommandAppender) error { + if f.value == "" { + return nil + } + appendValue, appendFlagValue := f.selectAppenderFuncs(cli) + if f.flag == "" { + appendValue(f.value) + } else { + appendFlagValue(f.flag, f.value) + } + return nil +} + +// selectAppenderFuncs returns the appropriate appender functions based on the redacted flag. +func (f stringFlag) selectAppenderFuncs(cli safecli.CommandAppender) (appenderFunc, appenderFunc) { + if f.redacted { + return cli.AppendRedacted, cli.AppendRedactedKV + } + return cli.AppendLoggable, cli.AppendLoggableKV +} + +// newStringFlag creates a new string flag with a given flag name and value. +func newStringFlag(flag, val string, redacted bool) Applier { + if flag == "" && val == "" { + return ErrorFlag(cli.ErrInvalidFlag) + } + return stringFlag{flag: flag, value: val, redacted: redacted} +} + +// NewStringFlag creates a new string flag with a given flag name and value. +func NewStringFlag(flag, val string) Applier { + return newStringFlag(flag, val, false) +} + +// NewRedactedStringFlag creates a new string flag with a given flag name and value. +func NewRedactedStringFlag(flag, val string) Applier { + return newStringFlag(flag, val, true) +} + +// NewStringArgument creates a new string argument with a given value. +func NewStringArgument(val string) Applier { + return newStringFlag("", val, false) +} diff --git a/pkg/kopia/cli/internal/test/flag_suite.go b/pkg/kopia/cli/internal/test/flag_suite.go new file mode 100644 index 0000000000..7c41a1ea3a --- /dev/null +++ b/pkg/kopia/cli/internal/test/flag_suite.go @@ -0,0 +1,112 @@ +package test + +import ( + "strings" + + "gopkg.in/check.v1" + + "github.com/pkg/errors" + + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" +) + +// FlagTest defines a single test for a flag. +type FlagTest struct { + // Name of the test. (required) + Name string + + // Flag to test. (required) + Flag flag.Applier + + // Expected CLI arguments. (optional) + ExpectedCLI []string + + // Expected log output. (optional) + // if empty, it will be set to ExpectedCLI joined with space. + // if empty and ExpectedCLI is empty, it will be ignored. + ExpectedLog string + + // Expected error. (optional) + // If nil, no error is expected and + // ExpectedCLI and ExpectedLog are checked. + ExpectedErr error +} + +// CheckCommentString implements check.CommentInterface +func (t *FlagTest) CheckCommentString() string { + return t.Name +} + +// setDefaultExpectedLog sets the default value for ExpectedLog based on ExpectedCLI. +func (t *FlagTest) setDefaultExpectedLog() { + if len(t.ExpectedLog) == 0 && len(t.ExpectedCLI) > 0 { + t.ExpectedLog = strings.Join(t.ExpectedCLI, " ") + } +} + +// assertError checks the error against ExpectedErr. +func (t *FlagTest) assertError(c *check.C, err error) { + if actualErr := errors.Cause(err); actualErr != nil { + c.Assert(actualErr, check.Equals, t.ExpectedErr, t) + } else { + c.Assert(err, check.Equals, t.ExpectedErr, t) + } +} + +// assertNoError makes sure there is no error. +func (t *FlagTest) assertNoError(c *check.C, err error) { + c.Assert(err, check.IsNil, t) +} + +// assertCLI asserts the builder's CLI output against ExpectedCLI. +func (t *FlagTest) assertCLI(c *check.C, b *safecli.Builder) { + c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) +} + +// assertLog asserts the builder's log output against ExpectedLog. +func (t *FlagTest) assertLog(c *check.C, b *safecli.Builder) { + t.setDefaultExpectedLog() + c.Check(b.String(), check.Equals, t.ExpectedLog, t) +} + +// Test runs the flag test. +func (ft *FlagTest) Test(c *check.C, b *safecli.Builder) { + err := flag.Apply(b, ft.Flag) + if ft.ExpectedErr != nil { + ft.assertError(c, err) + } else { + ft.assertNoError(c, err) + ft.assertCLI(c, b) + ft.assertLog(c, b) + } +} + +// FlagSuite defines a test suite for flags. +type FlagSuite struct { + Cmd string // Cmd appends to the safecli.Builder before test if not empty. + Tests []FlagTest // Tests to run. +} + +// TestFlags runs all tests in the flag suite. +func (s *FlagSuite) TestFlags(c *check.C) { + for _, test := range s.Tests { + b := newBuilder(s.Cmd) + test.Test(c, b) + } +} + +// NewFlagSuite creates a new FlagSuite. +func NewFlagSuite(tests []FlagTest) *FlagSuite { + return &FlagSuite{Tests: tests} +} + +// newBuilder creates a new safecli.Builder with the given command. +func newBuilder(cmd string) *safecli.Builder { + builder := safecli.NewBuilder() + if cmd != "" { + builder.AppendLoggable(cmd) + } + return builder +} From 0f635ad33638621c3e81849de07983c60192c73e Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Sat, 3 Feb 2024 16:03:14 -0800 Subject: [PATCH 03/22] apply go fmt Signed-off-by: pavel.larkin --- pkg/kopia/cli/internal/flag/flag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go index 898ae31e16..66148034a1 100644 --- a/pkg/kopia/cli/internal/flag/flag.go +++ b/pkg/kopia/cli/internal/flag/flag.go @@ -68,7 +68,7 @@ func (f simpleFlag) Apply(safecli.CommandAppender) error { } // EmptyFlag creates a new flag that does nothing. -// It is useful for creating a no-op flag when a condition is not met +// It is useful for creating a no-op flag when a condition is not met // but Applier interface is required. func EmptyFlag() Applier { return simpleFlag{} From d0a6dd13afeb5f54cff4448c74131522680dda5c Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Mon, 5 Feb 2024 17:51:24 -0800 Subject: [PATCH 04/22] Add common Kopia args and flags --- pkg/kopia/cli/args.go | 34 +++ pkg/kopia/cli/errors.go | 6 + .../cli/internal/flag/common/common_flags.go | 205 ++++++++++++++++++ .../internal/flag/common/common_flags_test.go | 196 +++++++++++++++++ pkg/kopia/cli/internal/test/flag_suite.go | 25 +-- .../cli/internal/test/flag_suite_test.go | 146 +++++++++++++ pkg/kopia/cli/internal/test/redact.go | 47 ++++ pkg/kopia/cli/internal/test/redact_test.go | 51 +++++ 8 files changed, 695 insertions(+), 15 deletions(-) create mode 100644 pkg/kopia/cli/args.go create mode 100644 pkg/kopia/cli/internal/flag/common/common_flags.go create mode 100644 pkg/kopia/cli/internal/flag/common/common_flags_test.go create mode 100644 pkg/kopia/cli/internal/test/flag_suite_test.go create mode 100644 pkg/kopia/cli/internal/test/redact.go create mode 100644 pkg/kopia/cli/internal/test/redact_test.go diff --git a/pkg/kopia/cli/args.go b/pkg/kopia/cli/args.go new file mode 100644 index 0000000000..78b697b7a9 --- /dev/null +++ b/pkg/kopia/cli/args.go @@ -0,0 +1,34 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +// The common arguments for Kopia CLI. + +// CommonArgs provides the common arguments for Kopia CLI. +type CommonArgs struct { + ConfigFilePath string // the path to the config file. + LogDirectory string // the directory where logs are stored. + LogLevel string // the level of logging. Default is "error". + RepoPassword string // the password for the repository. +} + +// CacheArgs provides the cache arguments for Kopia CLI. +type CacheArgs struct { + CacheDirectory string // the directory where cache is stored. Default is "/tmp/kopia-cache". + ContentCacheSizeMB int // the size of the content cache in MB. + ContentCacheSizeLimitMB int // the maximum size of the content cache in MB. + MetadataCacheSizeMB int // the size of the metadata cache in MB. + MetadataCacheSizeLimitMB int // the maximum size of the metadata cache in MB. +} diff --git a/pkg/kopia/cli/errors.go b/pkg/kopia/cli/errors.go index 946dd04e87..265c694d29 100644 --- a/pkg/kopia/cli/errors.go +++ b/pkg/kopia/cli/errors.go @@ -22,4 +22,10 @@ import ( var ( // ErrInvalidFlag is returned when the flag name is empty. ErrInvalidFlag = errors.New("invalid flag") + // ErrInvalidCommonArgs is returned when the common flag expects at most one cli.CommonArgs argument. + ErrInvalidCommonArgs = errors.New("common flag expects at most one cli.CommonArgs argument") + // ErrInvalidCacheArgs is returned when the cache flag expects at most one cli.CacheArgs argument. + ErrInvalidCacheArgs = errors.New("cache flag expects at most one cli.CacheArgs argument") + // ErrInvalidID is returned when the ID is empty. + ErrInvalidID = errors.New("invalid ID") ) diff --git a/pkg/kopia/cli/internal/flag/common/common_flags.go b/pkg/kopia/cli/internal/flag/common/common_flags.go new file mode 100644 index 0000000000..6df9b1bb5c --- /dev/null +++ b/pkg/kopia/cli/internal/flag/common/common_flags.go @@ -0,0 +1,205 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "strconv" + + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" +) + +// Flags without conditions which applied to different kopia commands. +var ( + All = flag.NewBoolFlag("--all", true) + Delta = flag.NewBoolFlag("--delta", true) + ShowIdentical = flag.NewBoolFlag("--show-identical", true) + NoGRPC = flag.NewBoolFlag("--no-grpc", true) +) + +// predefined flags +var ( + CheckForUpdates = checkForUpdates{CheckForUpdates: true} + NoCheckForUpdates = checkForUpdates{CheckForUpdates: false} +) + +// flag defaults +var ( + defaultLogLevel = "error" + defaultCacheDirectory = "/tmp/kopia-cache" +) + +// LogDirectory creates a new log directory flag with a given directory. +func LogDirectory(dir string) flag.Applier { + return flag.NewStringFlag("--log-dir", dir) +} + +// LogLevel creates a new log level flag with a given level. +// If the level is empty, the default log level is used. +func LogLevel(level string) flag.Applier { + if level == "" { + level = defaultLogLevel + } + return flag.NewStringFlag("--log-level", level) +} + +// CacheDirectory creates a new cache directory flag with a given directory. +// If the directory is empty, the default cache directory is used. +func CacheDirectory(dir string) flag.Applier { + if dir == "" { + dir = defaultCacheDirectory + } + return flag.NewStringFlag("--cache-directory", dir) +} + +// ConfigFilePath creates a new config file path flag with a given path. +func ConfigFilePath(path string) flag.Applier { + return flag.NewStringFlag("--config-file", path) +} + +// RepoPassword creates a new repository password flag with a given password. +func RepoPassword(password string) flag.Applier { + return flag.NewRedactedStringFlag("--password", password) +} + +// checkForUpdates is the flag for checking for updates. +type checkForUpdates struct { + CheckForUpdates bool +} + +func (f checkForUpdates) Flag() string { + if f.CheckForUpdates { + return "--check-for-updates" + } + return "--no-check-for-updates" +} + +func (f checkForUpdates) Apply(cli safecli.CommandAppender) error { + cli.AppendLoggable(f.Flag()) + return nil +} + +// ReadOnly creates a new read only flag. +func ReadOnly(readOnly bool) flag.Applier { + return flag.NewBoolFlag("--readonly", readOnly) +} + +// ContentCacheSizeLimitMB creates a new content cache size flag with a given size. +func ContentCacheSizeLimitMB(size int) flag.Applier { + val := strconv.Itoa(size) + return flag.NewStringFlag("--content-cache-size-limit-mb", val) +} + +// ContentCacheSizeMB creates a new content cache size flag with a given size. +func ContentCacheSizeMB(size int) flag.Applier { + val := strconv.Itoa(size) + return flag.NewStringFlag("--content-cache-size-mb", val) +} + +// MetadataCacheSizeLimitMB creates a new metadata cache size flag with a given size. +func MetadataCacheSizeLimitMB(size int) flag.Applier { + val := strconv.Itoa(size) + return flag.NewStringFlag("--metadata-cache-size-limit-mb", val) +} + +// MetadataCacheSizeMB creates a new metadata cache size flag with a given size. +func MetadataCacheSizeMB(size int) flag.Applier { + val := strconv.Itoa(size) + return flag.NewStringFlag("--metadata-cache-size-mb", val) +} + +// common are the global flags for Kopia. +type common struct { + cli.CommonArgs +} + +// Apply applies the global flags to the command. +func (f common) Apply(cmd safecli.CommandAppender) error { + return flag.Apply(cmd, + ConfigFilePath(f.ConfigFilePath), + LogLevel(f.LogLevel), + LogDirectory(f.LogDirectory), + RepoPassword(f.RepoPassword), + ) +} + +// Common creates a new common flag. +// If no arguments are provided, the default common flags are used. +// If one argument is provided, the common flags are used. +// If more than one argument is provided, ErrInvalidCommonArgs is returned. +func Common(args ...cli.CommonArgs) flag.Applier { + switch len(args) { + case 0: + return common{cli.CommonArgs{}} + case 1: + return common{args[0]} + default: + return flag.ErrorFlag(cli.ErrInvalidCommonArgs) + } +} + +// cache defines cache flags and implements Applier interface for the cache flags. +type cache struct { + cli.CacheArgs +} + +// Apply applies the cache flags to the command. +func (f cache) Apply(cmd safecli.CommandAppender) error { + return flag.Apply(cmd, + CacheDirectory(f.CacheDirectory), + ContentCacheSizeLimitMB(f.ContentCacheSizeLimitMB), + MetadataCacheSizeLimitMB(f.MetadataCacheSizeLimitMB), + // ContentCacheSizeMB(f.ContentCacheSizeMB), + // MetadataCacheSizeMB(f.MetadataCacheSizeMB), + ) +} + +// Cache creates a new cache flag. +// If no arguments are provided, the default cache flags are used. +// If one argument is provided, the cache flags are used. +// If more than one argument is provided, ErrInvalidCacheArgs is returned. +func Cache(args ...cli.CacheArgs) flag.Applier { + switch len(args) { + case 0: + return cache{cli.CacheArgs{}} + case 1: + return cache{args[0]} + default: + return flag.ErrorFlag(cli.ErrInvalidCacheArgs) + } +} + +// JSONOutput creates a new JSON output flag. +func JSONOutput(enable bool) flag.Applier { + return flag.NewBoolFlag("--json", enable) +} + +// JSON flag enables JSON output for different kopia commands. +var JSON = JSONOutput(true) + +// Delete creates a new delete flag. +func Delete(enable bool) flag.Applier { + return flag.NewBoolFlag("--delete", enable) +} + +// ID create the Kopia ID argument for different commands. +func ID(id string) flag.Applier { + if id == "" { + return flag.ErrorFlag(cli.ErrInvalidID) + } + return flag.NewStringArgument(id) +} diff --git a/pkg/kopia/cli/internal/flag/common/common_flags_test.go b/pkg/kopia/cli/internal/flag/common/common_flags_test.go new file mode 100644 index 0000000000..2e8ac8cfc1 --- /dev/null +++ b/pkg/kopia/cli/internal/flag/common/common_flags_test.go @@ -0,0 +1,196 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "fmt" + "testing" + + "github.com/kanisterio/kanister/pkg/kopia/cli" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/test" + "gopkg.in/check.v1" +) + +func TestCommonFlags(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(test.NewFlagSuite([]test.FlagTest{ + { + Name: "Empty LogDirectory should generate a flag with default value", + Flag: LogDirectory(""), + }, + { + Name: "LogDirectory with value should generate a flag with the given directory", + Flag: LogDirectory("/path/to/logs"), + ExpectedCLI: []string{"--log-dir=/path/to/logs"}, + }, + { + Name: "Empty LogLevel should generate a flag with default value", + Flag: LogLevel(""), + ExpectedCLI: []string{fmt.Sprintf("--log-level=%s", defaultLogLevel)}, + }, + { + Name: "LogLevel with value should generate a flag with the given level", + Flag: LogLevel("info"), + ExpectedCLI: []string{"--log-level=info"}, + }, + { + Name: "Empty CacheDirectory should generate a flag with default value", + Flag: CacheDirectory(""), + ExpectedCLI: []string{fmt.Sprintf("--cache-directory=%s", defaultCacheDirectory)}, + }, + { + Name: "CacheDirectory with value should generate a flag with the given directory", + Flag: CacheDirectory("/home/user/.cache/kopia"), + ExpectedCLI: []string{"--cache-directory=/home/user/.cache/kopia"}, + }, + { + Name: "Empty ConfigFilePath should not generate a flag", + Flag: ConfigFilePath(""), + }, + { + Name: "ConfigFilePath with value should generate a flag with the given config file path", + Flag: ConfigFilePath("/var/kopia/config"), + ExpectedCLI: []string{"--config-file=/var/kopia/config"}, + }, + { + Name: "Empty RepoPassword should not generate a flag", + Flag: RepoPassword(""), + }, + { + Name: "RepoPassword with value should generate a flag with the given value and redact it for logs", + Flag: RepoPassword("pass12345"), + ExpectedCLI: []string{"--password=pass12345"}, + }, + { + Name: "CheckForUpdates should always generate a flag", + Flag: CheckForUpdates, + ExpectedCLI: []string{"--check-for-updates"}, + }, + { + Name: "NoCheckForUpdates should always generate a flag", + Flag: NoCheckForUpdates, + ExpectedCLI: []string{"--no-check-for-updates"}, + }, + { + Name: "ReadOnly(false)should not generate a flag", + Flag: ReadOnly(false), + }, + { + Name: "ReadOnly(true) should generate a flag", + Flag: ReadOnly(true), + ExpectedCLI: []string{"--readonly"}, + }, + { + Name: "NoGRPC should always generate '--no-grpc' flag", + Flag: NoGRPC, + ExpectedCLI: []string{"--no-grpc"}, + }, + { + Name: "JSON should always generate a flag", + Flag: JSON, + ExpectedCLI: []string{"--json"}, + }, + { + Name: "ContentCacheSizeLimitMB with value should generate a flag with the given value", + Flag: ContentCacheSizeLimitMB(1024), + ExpectedCLI: []string{"--content-cache-size-limit-mb=1024"}, + }, + { + Name: "ContentCacheSizeMB with value should generate a flag with the given value", + Flag: ContentCacheSizeMB(1024), + ExpectedCLI: []string{"--content-cache-size-mb=1024"}, + }, + { + Name: "MetadataCacheSizeLimitMB with value should generate a flag with the given value", + Flag: MetadataCacheSizeLimitMB(1024), + ExpectedCLI: []string{"--metadata-cache-size-limit-mb=1024"}, + }, + { + Name: "MetadataCacheSizeMB with value should generate a flag with the given value", + Flag: MetadataCacheSizeMB(1024), + ExpectedCLI: []string{"--metadata-cache-size-mb=1024"}, + }, + { + Name: "Empty Common should generate a flag with default value(s)", + Flag: Common(), + ExpectedCLI: []string{"--log-level=error"}, + }, + { + Name: "Common with more than one cli.CommonArgs should generate an error", + Flag: Common(cli.CommonArgs{}, cli.CommonArgs{}), + ExpectedErr: cli.ErrInvalidCommonArgs, + }, + { + Name: "Common with values should generate multiple flags with the given values and redact password for logs", + Flag: Common(cli.CommonArgs{ + ConfigFilePath: "/var/kopia/config", + LogDirectory: "/var/log/kopia", + LogLevel: "info", + RepoPassword: "pass12345", + }), + ExpectedCLI: []string{ + "--config-file=/var/kopia/config", + "--log-level=info", + "--log-dir=/var/log/kopia", + "--password=pass12345", + }, + }, + { + Name: "Empty FlagCacheArgs should generate multiple flags with default values", + Flag: Cache(), + ExpectedCLI: []string{ + "--cache-directory=/tmp/kopia-cache", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + }, + }, + { + Name: "Cache with more than one cli.CacheArgs should generate an error", + Flag: Cache(cli.CacheArgs{}, cli.CacheArgs{}), + ExpectedErr: cli.ErrInvalidCacheArgs, + }, + { + Name: "Cache with CacheArgs should generate multiple cache related flags", + Flag: Cache(cli.CacheArgs{ + CacheDirectory: "/home/user/.cache/kopia", + ContentCacheSizeLimitMB: 1024, + MetadataCacheSizeLimitMB: 2048, + }), + ExpectedCLI: []string{ + "--cache-directory=/home/user/.cache/kopia", + "--content-cache-size-limit-mb=1024", + "--metadata-cache-size-limit-mb=2048", + }, + }, + { + Name: "Delete(false) should not generate a flag", + Flag: Delete(false), + }, + { + Name: "Delete(true) should generate a flag", + Flag: Delete(true), + ExpectedCLI: []string{"--delete"}, + }, + { + Name: "Empty ID should generate an ErrInvalidID error", + Flag: ID(""), + ExpectedErr: cli.ErrInvalidID, + }, + { + Name: "ID with value should generate an argument with the given value", + Flag: ID("id12345"), + ExpectedCLI: []string{"id12345"}, + }, +})) diff --git a/pkg/kopia/cli/internal/test/flag_suite.go b/pkg/kopia/cli/internal/test/flag_suite.go index 7c41a1ea3a..c930573466 100644 --- a/pkg/kopia/cli/internal/test/flag_suite.go +++ b/pkg/kopia/cli/internal/test/flag_suite.go @@ -1,8 +1,6 @@ package test import ( - "strings" - "gopkg.in/check.v1" "github.com/pkg/errors" @@ -42,17 +40,14 @@ func (t *FlagTest) CheckCommentString() string { // setDefaultExpectedLog sets the default value for ExpectedLog based on ExpectedCLI. func (t *FlagTest) setDefaultExpectedLog() { if len(t.ExpectedLog) == 0 && len(t.ExpectedCLI) > 0 { - t.ExpectedLog = strings.Join(t.ExpectedCLI, " ") + t.ExpectedLog = RedactCLI(t.ExpectedCLI) } } // assertError checks the error against ExpectedErr. func (t *FlagTest) assertError(c *check.C, err error) { - if actualErr := errors.Cause(err); actualErr != nil { - c.Assert(actualErr, check.Equals, t.ExpectedErr, t) - } else { - c.Assert(err, check.Equals, t.ExpectedErr, t) - } + actualErr := errors.Cause(err) + c.Assert(actualErr, check.Equals, t.ExpectedErr, t) } // assertNoError makes sure there is no error. @@ -72,14 +67,14 @@ func (t *FlagTest) assertLog(c *check.C, b *safecli.Builder) { } // Test runs the flag test. -func (ft *FlagTest) Test(c *check.C, b *safecli.Builder) { - err := flag.Apply(b, ft.Flag) - if ft.ExpectedErr != nil { - ft.assertError(c, err) +func (t *FlagTest) Test(c *check.C, b *safecli.Builder) { + err := flag.Apply(b, t.Flag) + if t.ExpectedErr != nil { + t.assertError(c, err) } else { - ft.assertNoError(c, err) - ft.assertCLI(c, b) - ft.assertLog(c, b) + t.assertNoError(c, err) + t.assertCLI(c, b) + t.assertLog(c, b) } } diff --git a/pkg/kopia/cli/internal/test/flag_suite_test.go b/pkg/kopia/cli/internal/test/flag_suite_test.go new file mode 100644 index 0000000000..307ff62533 --- /dev/null +++ b/pkg/kopia/cli/internal/test/flag_suite_test.go @@ -0,0 +1,146 @@ +package test_test + +import ( + "strings" + "testing" + + "github.com/pkg/errors" + + "gopkg.in/check.v1" + + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/test" + "github.com/kanisterio/safecli" +) + +func TestCustomFlag(t *testing.T) { check.TestingT(t) } + +// CustomFlagTest is a test for FlagTest. +// it has a custom flag that can be used to test the flag. +// and implements flag.Applier. +type CustomFlagTest struct { + name string + flag string + flagErr error + expectedErr error +} + +func (t *CustomFlagTest) Apply(cli safecli.CommandAppender) error { + if t.flagErr == nil { + cli.AppendLoggable(t.flag) + } + return t.flagErr +} + +func (t *CustomFlagTest) Test(c *check.C) { + flagTest := test.FlagTest{ + Name: t.name, + Flag: t, + ExpectedErr: t.expectedErr, + } + if t.flag != "" { + flagTest.ExpectedCLI = []string{t.flag} + } + b := safecli.NewBuilder() + flagTest.Test(c, b) +} + +type CustomFlagSuite struct { + cmd string + tests []test.FlagTest +} + +func (s *CustomFlagSuite) Test(c *check.C) { + suite := test.NewFlagSuite(s.tests) + suite.Cmd = s.cmd + suite.TestFlags(c) +} + +// TestRunnerWithConfig is a test suite for CustomFlagTest. +type TestRunnerWithConfig struct { + out strings.Builder // output buffer for the test results + cfg *check.RunConf // custom test configuration +} + +// register the test suite +var _ = check.Suite(&TestRunnerWithConfig{}) + +// SetUpTest sets up the test suite for running. +// it initializes the output buffer and the test configuration. +func (s *TestRunnerWithConfig) SetUpTest(c *check.C) { + s.out = strings.Builder{} + s.cfg = &check.RunConf{ + Output: &s.out, + Verbose: true, + } +} + +// TestFlagTestOK tests the FlagTest with no errors. +func (s *TestRunnerWithConfig) TestFlagTestOK(c *check.C) { + cft := CustomFlagTest{ + name: "TestFlagOK", + flag: "--test", + } + res := check.Run(&cft, s.cfg) + c.Assert(s.out.String(), check.Matches, "PASS: .*CustomFlagTest\\.Test.*\n") + c.Assert(res.Passed(), check.Equals, true) +} + +// TestFlagTestErr tests the FlagTest with an error. +func (s *TestRunnerWithConfig) TestFlagTestErr(c *check.C) { + err := errors.New("test error") + cft := CustomFlagTest{ + name: "TestFlagErr", + flagErr: err, + expectedErr: err, + } + res := check.Run(&cft, s.cfg) + c.Assert(s.out.String(), check.Matches, "PASS: .*CustomFlagTest\\.Test.*\n") + c.Assert(res.Passed(), check.Equals, true) +} + +// TestFlagTestWrapperErr tests the FlagTest with a wrapped error. +func (s *TestRunnerWithConfig) TestFlagTestWrapperErr(c *check.C) { + err := errors.New("test error") + werr := errors.Wrap(err, "wrapper error") + cft := CustomFlagTest{ + name: "TestFlagTestWrapperErr", + flagErr: werr, + expectedErr: err, + } + res := check.Run(&cft, s.cfg) + c.Assert(s.out.String(), check.Matches, "PASS: .*CustomFlagTest\\.Test.*\n") + c.Assert(res.Passed(), check.Equals, true) +} + +// TestFlagTestUnexpectedErr tests the FlagTest with an unexpected error. +func (s *TestRunnerWithConfig) TestFlagTestUnexpectedErr(c *check.C) { + err := errors.New("test error") + cft := CustomFlagTest{ + name: "TestFlagUnexpectedErr", + flag: "--test", + flagErr: err, + expectedErr: nil, + } + res := check.Run(&cft, s.cfg) + ss := s.out.String() + c.Assert(strings.Contains(ss, "TestFlagUnexpectedErr"), check.Equals, true) + c.Assert(strings.Contains(ss, "test error"), check.Equals, true) + c.Assert(res.Passed(), check.Equals, false) +} + +// TestFlagSuiteOK tests the FlagSuite with no errors. +func (s *TestRunnerWithConfig) TestFlagSuiteOK(c *check.C) { + cfs := CustomFlagSuite{ + cmd: "cmd", + tests: []test.FlagTest{ + { + Name: "TestFlagOK", + Flag: &CustomFlagTest{name: "TestFlagOK", flag: "--test"}, + ExpectedCLI: []string{"cmd", "--test"}, + }, + }, + } + res := check.Run(&cfs, s.cfg) + c.Assert(s.out.String(), check.Matches, "PASS: .*CustomFlagSuite\\.Test.*\n") + c.Assert(res.Passed(), check.Equals, true) +} diff --git a/pkg/kopia/cli/internal/test/redact.go b/pkg/kopia/cli/internal/test/redact.go new file mode 100644 index 0000000000..2622877d13 --- /dev/null +++ b/pkg/kopia/cli/internal/test/redact.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "fmt" + "strings" +) + +const ( + redactField = "<****>" +) + +var redactedFlags = []string{ + "--password", + "--user-password", + "--server-password", + "--server-control-password", + "--server-cert-fingerprint", +} + +// RedactCLI redacts sensitive information from the CLI command for tests. +func RedactCLI(cli []string) string { + redactedCLI := make([]string, len(cli)) + for i, arg := range cli { + redactedCLI[i] = arg + for _, flag := range redactedFlags { + if strings.HasPrefix(arg, flag+"=") { + redactedCLI[i] = fmt.Sprintf("%s=%s", flag, redactField) + break // redacted flag found, no need to check further + } + } + } + return strings.Join(redactedCLI, " ") +} \ No newline at end of file diff --git a/pkg/kopia/cli/internal/test/redact_test.go b/pkg/kopia/cli/internal/test/redact_test.go new file mode 100644 index 0000000000..4bce54b009 --- /dev/null +++ b/pkg/kopia/cli/internal/test/redact_test.go @@ -0,0 +1,51 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "strings" + "testing" + + "gopkg.in/check.v1" +) + +func TestRedactCLI(t *testing.T) { check.TestingT(t) } + +type RedactSuite struct{} + +var _ = check.Suite(&RedactSuite{}) + +func (s *RedactSuite) TestRedactCLI(c *check.C) { + cli := []string{ + "--password=secret", + "--user-password=123456", + "--server-password=pass123", + "--server-control-password=abc123", + "--server-cert-fingerprint=abcd1234", + "--other-flag=value", + "argument", + } + expected := []string{ + "--password=<****>", + "--user-password=<****>", + "--server-password=<****>", + "--server-control-password=<****>", + "--server-cert-fingerprint=<****>", + "--other-flag=value", + "argument", + } + result := RedactCLI(cli) + c.Assert(result, check.Equals, strings.Join(expected, " ")) +} From 9850f4a450fe5a0ce37ab18189987b88d05dce00 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Mon, 12 Feb 2024 12:11:54 -0800 Subject: [PATCH 05/22] Fix Apply and test.Suit Signed-off-by: pavel.larkin --- pkg/kopia/cli/internal/flag/flag.go | 2 +- pkg/kopia/cli/internal/test/flag_suite.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go index 66148034a1..39a357be53 100644 --- a/pkg/kopia/cli/internal/flag/flag.go +++ b/pkg/kopia/cli/internal/flag/flag.go @@ -36,7 +36,7 @@ func Apply(cli safecli.CommandAppender, flags ...Applier) error { if flag == nil { // if the flag is nil, skip it continue } - if err := flag.Apply(cli); err != nil { + if err := flag.Apply(sub); err != nil { return err } } diff --git a/pkg/kopia/cli/internal/test/flag_suite.go b/pkg/kopia/cli/internal/test/flag_suite.go index 7c41a1ea3a..7fe60d909c 100644 --- a/pkg/kopia/cli/internal/test/flag_suite.go +++ b/pkg/kopia/cli/internal/test/flag_suite.go @@ -62,7 +62,9 @@ func (t *FlagTest) assertNoError(c *check.C, err error) { // assertCLI asserts the builder's CLI output against ExpectedCLI. func (t *FlagTest) assertCLI(c *check.C, b *safecli.Builder) { - c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) + if t.ExpectedCLI != nil { + c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) + } } // assertLog asserts the builder's log output against ExpectedLog. @@ -74,11 +76,11 @@ func (t *FlagTest) assertLog(c *check.C, b *safecli.Builder) { // Test runs the flag test. func (ft *FlagTest) Test(c *check.C, b *safecli.Builder) { err := flag.Apply(b, ft.Flag) + ft.assertCLI(c, b) if ft.ExpectedErr != nil { ft.assertError(c, err) } else { ft.assertNoError(c, err) - ft.assertCLI(c, b) ft.assertLog(c, b) } } From 246e1c1aac53b73be773197f8df1a9748a76970a Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Mon, 12 Feb 2024 13:21:04 -0800 Subject: [PATCH 06/22] Remove variadic args for Common and Cache flags Signed-off-by: pavel.larkin --- .../cli/internal/flag/common/common_flags.go | 28 +++---------------- .../internal/flag/common/common_flags_test.go | 14 ++-------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/pkg/kopia/cli/internal/flag/common/common_flags.go b/pkg/kopia/cli/internal/flag/common/common_flags.go index 6df9b1bb5c..3c3000885f 100644 --- a/pkg/kopia/cli/internal/flag/common/common_flags.go +++ b/pkg/kopia/cli/internal/flag/common/common_flags.go @@ -138,18 +138,8 @@ func (f common) Apply(cmd safecli.CommandAppender) error { } // Common creates a new common flag. -// If no arguments are provided, the default common flags are used. -// If one argument is provided, the common flags are used. -// If more than one argument is provided, ErrInvalidCommonArgs is returned. -func Common(args ...cli.CommonArgs) flag.Applier { - switch len(args) { - case 0: - return common{cli.CommonArgs{}} - case 1: - return common{args[0]} - default: - return flag.ErrorFlag(cli.ErrInvalidCommonArgs) - } +func Common(args cli.CommonArgs) flag.Applier { + return common{args} } // cache defines cache flags and implements Applier interface for the cache flags. @@ -169,18 +159,8 @@ func (f cache) Apply(cmd safecli.CommandAppender) error { } // Cache creates a new cache flag. -// If no arguments are provided, the default cache flags are used. -// If one argument is provided, the cache flags are used. -// If more than one argument is provided, ErrInvalidCacheArgs is returned. -func Cache(args ...cli.CacheArgs) flag.Applier { - switch len(args) { - case 0: - return cache{cli.CacheArgs{}} - case 1: - return cache{args[0]} - default: - return flag.ErrorFlag(cli.ErrInvalidCacheArgs) - } +func Cache(args cli.CacheArgs) flag.Applier { + return cache{args} } // JSONOutput creates a new JSON output flag. diff --git a/pkg/kopia/cli/internal/flag/common/common_flags_test.go b/pkg/kopia/cli/internal/flag/common/common_flags_test.go index 2e8ac8cfc1..a7d03f571f 100644 --- a/pkg/kopia/cli/internal/flag/common/common_flags_test.go +++ b/pkg/kopia/cli/internal/flag/common/common_flags_test.go @@ -124,14 +124,9 @@ var _ = check.Suite(test.NewFlagSuite([]test.FlagTest{ }, { Name: "Empty Common should generate a flag with default value(s)", - Flag: Common(), + Flag: Common(cli.CommonArgs{}), ExpectedCLI: []string{"--log-level=error"}, }, - { - Name: "Common with more than one cli.CommonArgs should generate an error", - Flag: Common(cli.CommonArgs{}, cli.CommonArgs{}), - ExpectedErr: cli.ErrInvalidCommonArgs, - }, { Name: "Common with values should generate multiple flags with the given values and redact password for logs", Flag: Common(cli.CommonArgs{ @@ -149,18 +144,13 @@ var _ = check.Suite(test.NewFlagSuite([]test.FlagTest{ }, { Name: "Empty FlagCacheArgs should generate multiple flags with default values", - Flag: Cache(), + Flag: Cache(cli.CacheArgs{}), ExpectedCLI: []string{ "--cache-directory=/tmp/kopia-cache", "--content-cache-size-limit-mb=0", "--metadata-cache-size-limit-mb=0", }, }, - { - Name: "Cache with more than one cli.CacheArgs should generate an error", - Flag: Cache(cli.CacheArgs{}, cli.CacheArgs{}), - ExpectedErr: cli.ErrInvalidCacheArgs, - }, { Name: "Cache with CacheArgs should generate multiple cache related flags", Flag: Cache(cli.CacheArgs{ From 24707e513c3322a79d1e61c0a24d814153b1de8d Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 16 Feb 2024 16:51:58 -0800 Subject: [PATCH 07/22] pkg/kopia/cli/internal/flag is implemented in the safecli@v0.0.4 now Signed-off-by: pavel.larkin --- go.mod | 2 +- go.sum | 2 + pkg/kopia/cli/errors.go | 25 --- pkg/kopia/cli/internal/flag/bool_flag.go | 45 ------ pkg/kopia/cli/internal/flag/flag.go | 81 ---------- pkg/kopia/cli/internal/flag/flag_test.go | 171 --------------------- pkg/kopia/cli/internal/flag/string_flag.go | 78 ---------- pkg/kopia/cli/internal/test/flag_suite.go | 114 -------------- 8 files changed, 3 insertions(+), 515 deletions(-) delete mode 100644 pkg/kopia/cli/errors.go delete mode 100644 pkg/kopia/cli/internal/flag/bool_flag.go delete mode 100644 pkg/kopia/cli/internal/flag/flag.go delete mode 100644 pkg/kopia/cli/internal/flag/flag_test.go delete mode 100644 pkg/kopia/cli/internal/flag/string_flag.go delete mode 100644 pkg/kopia/cli/internal/test/flag_suite.go diff --git a/go.mod b/go.mod index 0b81ec2854..afa1376f0e 100644 --- a/go.mod +++ b/go.mod @@ -216,7 +216,7 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) -require github.com/kanisterio/safecli v0.0.3 +require github.com/kanisterio/safecli v0.0.4 require ( github.com/Azure/go-autorest/autorest v0.11.27 // indirect diff --git a/go.sum b/go.sum index fed0f3304c..8247482a7e 100644 --- a/go.sum +++ b/go.sum @@ -361,6 +361,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kanisterio/safecli v0.0.3 h1:ts3oRVSoRexFBv9pOdWYZsN4k068pWx5Cl4zN54mQro= github.com/kanisterio/safecli v0.0.3/go.mod h1:fK3Mcbeiso+NtkUdhGugK0Vf4S2l8ObvFf564ry1W5A= +github.com/kanisterio/safecli v0.0.4 h1:8pO7Zhm9YeW47XQZNRt/AaXj7kSPeIbaV7aRDRtHs4k= +github.com/kanisterio/safecli v0.0.4/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734/go.mod h1:rdqSnvOJuKCPFW/h2rVLuXOAkRnHHdp9PZcKx4HCoDM= github.com/kastenhq/stow v0.2.6-kasten.1.0.20231101232131-9321daa23aae h1:2cl4yuAJpdmLCx7G8eIsfNlQBLEfw0JDj6mTTyqc5qg= diff --git a/pkg/kopia/cli/errors.go b/pkg/kopia/cli/errors.go deleted file mode 100644 index 946dd04e87..0000000000 --- a/pkg/kopia/cli/errors.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cli - -import ( - "github.com/pkg/errors" -) - -// flag errors -var ( - // ErrInvalidFlag is returned when the flag name is empty. - ErrInvalidFlag = errors.New("invalid flag") -) diff --git a/pkg/kopia/cli/internal/flag/bool_flag.go b/pkg/kopia/cli/internal/flag/bool_flag.go deleted file mode 100644 index 4eea665cb2..0000000000 --- a/pkg/kopia/cli/internal/flag/bool_flag.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag - -import ( - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli" -) - -// boolFlag defines a boolean flag with a given flag name. -// If enabled is set to true, the flag is applied; otherwise, it is not. -type boolFlag struct { - flag string - enabled bool -} - -// Apply appends the flag to the command if the flag is enabled. -func (f boolFlag) Apply(cli safecli.CommandAppender) error { - if f.enabled { - cli.AppendLoggable(f.flag) - } - return nil -} - -// NewBoolFlag creates a new bool flag with a given flag name. -// If the flag name is empty, cli.ErrInvalidFlag is returned. -func NewBoolFlag(flag string, enabled bool) Applier { - if flag == "" { - return ErrorFlag(cli.ErrInvalidFlag) - } - return boolFlag{flag, enabled} -} diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go deleted file mode 100644 index 39a357be53..0000000000 --- a/pkg/kopia/cli/internal/flag/flag.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag - -import ( - "github.com/kanisterio/safecli" -) - -// Applier applies flags/args to the command. -type Applier interface { - // Apply applies the flags/args to the command. - Apply(cli safecli.CommandAppender) error -} - -// Apply appends multiple flags to the CLI. -// If any of the flags encounter an error during the Apply process, -// the error is returned and no changes are made to the CLI. -// If no error, the flags are appended to the CLI. -func Apply(cli safecli.CommandAppender, flags ...Applier) error { - // create a new sub builder which will be used to apply the flags - // to avoid mutating the CLI if an error is encountered. - sub := safecli.NewBuilder() - for _, flag := range flags { - if flag == nil { // if the flag is nil, skip it - continue - } - if err := flag.Apply(sub); err != nil { - return err - } - } - cli.Append(sub) - return nil -} - -// flags defines a collection of Flags. -type flags []Applier - -// Apply applies the flags to the CLI. -func (flags flags) Apply(cli safecli.CommandAppender) error { - return Apply(cli, flags...) -} - -// NewFlags creates a new collection of flags. -func NewFlags(fs ...Applier) Applier { - return flags(fs) -} - -// simpleFlag is a simple implementation of the Applier interface. -type simpleFlag struct { - err error -} - -// Apply does nothing except return an error if one is set. -func (f simpleFlag) Apply(safecli.CommandAppender) error { - return f.err -} - -// EmptyFlag creates a new flag that does nothing. -// It is useful for creating a no-op flag when a condition is not met -// but Applier interface is required. -func EmptyFlag() Applier { - return simpleFlag{} -} - -// ErrorFlag creates a new flag that returns an error when applied. -// It is useful for creating a flag validation if a condition is not met. -func ErrorFlag(err error) Applier { - return simpleFlag{err} -} diff --git a/pkg/kopia/cli/internal/flag/flag_test.go b/pkg/kopia/cli/internal/flag/flag_test.go deleted file mode 100644 index c20a46b450..0000000000 --- a/pkg/kopia/cli/internal/flag/flag_test.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag_test - -import ( - "errors" - "testing" - - "gopkg.in/check.v1" - - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/test" -) - -var ( - ErrFlag = errors.New("flag error") -) - -// MockFlagApplier is a mock implementation of the FlagApplier interface. -type MockFlagApplier struct { - flagName string - applyErr error -} - -func (m *MockFlagApplier) Apply(cli safecli.CommandAppender) error { - cli.AppendLoggable(m.flagName) - return m.applyErr -} - -func TestApply(t *testing.T) { check.TestingT(t) } - -var _ = check.Suite(&test.FlagSuite{Cmd: "cmd", Tests: []test.FlagTest{ - { - Name: "Apply with no flags should generate only the command", - ExpectedCLI: []string{"cmd"}, - }, - { - Name: "Apply with nil flags should generate only the command", - Flag: flag.NewFlags(nil, nil), - ExpectedCLI: []string{"cmd"}, - }, - { - Name: "Apply with flags should generate the command and flags", - Flag: flag.NewFlags( - &MockFlagApplier{flagName: "--flag1", applyErr: nil}, - &MockFlagApplier{flagName: "--flag2", applyErr: nil}, - ), - ExpectedCLI: []string{"cmd", "--flag1", "--flag2"}, - }, - { - Name: "Apply with one error flag should not modify the command and return the error", - Flag: flag.NewFlags( - &MockFlagApplier{flagName: "flag1", applyErr: nil}, - &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: ErrFlag, - }, - { - Name: "NewBoolFlag", - Flag: flag.NewFlags( - flag.NewBoolFlag("--flag1", true), - flag.NewBoolFlag("--flag2", false), - ), - ExpectedCLI: []string{"cmd", "--flag1"}, - }, - { - Name: "NewBoolFlag with empty flag name should return an error", - Flag: flag.NewFlags( - flag.NewBoolFlag("", true), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewStringFlag", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - flag.NewStringFlag("--flag2", ""), - ), - ExpectedCLI: []string{"cmd", "--flag1=value1"}, - }, - { - Name: "NewStringFlag with all empty values should return an error", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - flag.NewStringFlag("", ""), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewRedactedStringFlag", - Flag: flag.NewFlags( - flag.NewRedactedStringFlag("--flag1", "value1"), - flag.NewRedactedStringFlag("--flag2", ""), - flag.NewRedactedStringFlag("", "value3"), - ), - ExpectedCLI: []string{"cmd", "--flag1=value1", "value3"}, - ExpectedLog: "cmd --flag1=<****> <****>", - }, - { - Name: "NewRedactedStringFlag with all empty values should return an error", - Flag: flag.NewFlags( - flag.NewRedactedStringFlag("--flag1", "value1"), - flag.NewRedactedStringFlag("", ""), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewStringValue", - Flag: flag.NewFlags( - flag.NewStringArgument("value1"), - ), - ExpectedCLI: []string{"cmd", "value1"}, - }, - { - Name: "NewStringValue with empty value should return an error", - Flag: flag.NewFlags( - flag.NewStringArgument(""), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewFlags should generate multiple flags", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - flag.NewRedactedStringFlag("--flag2", "value2"), - flag.NewStringArgument("value3"), - ), - ExpectedCLI: []string{"cmd", "--flag1=value1", "--flag2=value2", "value3"}, - ExpectedLog: "cmd --flag1=value1 --flag2=<****> value3", - }, - { - Name: "NewFlags should generate no flags if one of them returns an error", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: ErrFlag, - }, - { - Name: "EmptyFlag should not generate any flags", - Flag: flag.EmptyFlag(), - ExpectedCLI: []string{"cmd"}, - }, - { - Name: "ErrorFlag should return an error", - Flag: flag.ErrorFlag(ErrFlag), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: ErrFlag, - }, -}}) diff --git a/pkg/kopia/cli/internal/flag/string_flag.go b/pkg/kopia/cli/internal/flag/string_flag.go deleted file mode 100644 index a94c29722c..0000000000 --- a/pkg/kopia/cli/internal/flag/string_flag.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag - -import ( - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli" -) - -// stringFlag defines a string flag with a given flag name and value. -// If the value is empty, the flag is not applied. -type stringFlag struct { - flag string // flag name - value string // flag value - redacted bool // output the value as redacted -} - -// appenderFunc is a function that appends strings to a command. -type appenderFunc func(...string) *safecli.Builder - -// Apply appends the flag to the command if the value is not empty. -// If the value is redacted, it is appended as redacted. -func (f stringFlag) Apply(cli safecli.CommandAppender) error { - if f.value == "" { - return nil - } - appendValue, appendFlagValue := f.selectAppenderFuncs(cli) - if f.flag == "" { - appendValue(f.value) - } else { - appendFlagValue(f.flag, f.value) - } - return nil -} - -// selectAppenderFuncs returns the appropriate appender functions based on the redacted flag. -func (f stringFlag) selectAppenderFuncs(cli safecli.CommandAppender) (appenderFunc, appenderFunc) { - if f.redacted { - return cli.AppendRedacted, cli.AppendRedactedKV - } - return cli.AppendLoggable, cli.AppendLoggableKV -} - -// newStringFlag creates a new string flag with a given flag name and value. -func newStringFlag(flag, val string, redacted bool) Applier { - if flag == "" && val == "" { - return ErrorFlag(cli.ErrInvalidFlag) - } - return stringFlag{flag: flag, value: val, redacted: redacted} -} - -// NewStringFlag creates a new string flag with a given flag name and value. -func NewStringFlag(flag, val string) Applier { - return newStringFlag(flag, val, false) -} - -// NewRedactedStringFlag creates a new string flag with a given flag name and value. -func NewRedactedStringFlag(flag, val string) Applier { - return newStringFlag(flag, val, true) -} - -// NewStringArgument creates a new string argument with a given value. -func NewStringArgument(val string) Applier { - return newStringFlag("", val, false) -} diff --git a/pkg/kopia/cli/internal/test/flag_suite.go b/pkg/kopia/cli/internal/test/flag_suite.go deleted file mode 100644 index 7fe60d909c..0000000000 --- a/pkg/kopia/cli/internal/test/flag_suite.go +++ /dev/null @@ -1,114 +0,0 @@ -package test - -import ( - "strings" - - "gopkg.in/check.v1" - - "github.com/pkg/errors" - - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" -) - -// FlagTest defines a single test for a flag. -type FlagTest struct { - // Name of the test. (required) - Name string - - // Flag to test. (required) - Flag flag.Applier - - // Expected CLI arguments. (optional) - ExpectedCLI []string - - // Expected log output. (optional) - // if empty, it will be set to ExpectedCLI joined with space. - // if empty and ExpectedCLI is empty, it will be ignored. - ExpectedLog string - - // Expected error. (optional) - // If nil, no error is expected and - // ExpectedCLI and ExpectedLog are checked. - ExpectedErr error -} - -// CheckCommentString implements check.CommentInterface -func (t *FlagTest) CheckCommentString() string { - return t.Name -} - -// setDefaultExpectedLog sets the default value for ExpectedLog based on ExpectedCLI. -func (t *FlagTest) setDefaultExpectedLog() { - if len(t.ExpectedLog) == 0 && len(t.ExpectedCLI) > 0 { - t.ExpectedLog = strings.Join(t.ExpectedCLI, " ") - } -} - -// assertError checks the error against ExpectedErr. -func (t *FlagTest) assertError(c *check.C, err error) { - if actualErr := errors.Cause(err); actualErr != nil { - c.Assert(actualErr, check.Equals, t.ExpectedErr, t) - } else { - c.Assert(err, check.Equals, t.ExpectedErr, t) - } -} - -// assertNoError makes sure there is no error. -func (t *FlagTest) assertNoError(c *check.C, err error) { - c.Assert(err, check.IsNil, t) -} - -// assertCLI asserts the builder's CLI output against ExpectedCLI. -func (t *FlagTest) assertCLI(c *check.C, b *safecli.Builder) { - if t.ExpectedCLI != nil { - c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) - } -} - -// assertLog asserts the builder's log output against ExpectedLog. -func (t *FlagTest) assertLog(c *check.C, b *safecli.Builder) { - t.setDefaultExpectedLog() - c.Check(b.String(), check.Equals, t.ExpectedLog, t) -} - -// Test runs the flag test. -func (ft *FlagTest) Test(c *check.C, b *safecli.Builder) { - err := flag.Apply(b, ft.Flag) - ft.assertCLI(c, b) - if ft.ExpectedErr != nil { - ft.assertError(c, err) - } else { - ft.assertNoError(c, err) - ft.assertLog(c, b) - } -} - -// FlagSuite defines a test suite for flags. -type FlagSuite struct { - Cmd string // Cmd appends to the safecli.Builder before test if not empty. - Tests []FlagTest // Tests to run. -} - -// TestFlags runs all tests in the flag suite. -func (s *FlagSuite) TestFlags(c *check.C) { - for _, test := range s.Tests { - b := newBuilder(s.Cmd) - test.Test(c, b) - } -} - -// NewFlagSuite creates a new FlagSuite. -func NewFlagSuite(tests []FlagTest) *FlagSuite { - return &FlagSuite{Tests: tests} -} - -// newBuilder creates a new safecli.Builder with the given command. -func newBuilder(cmd string) *safecli.Builder { - builder := safecli.NewBuilder() - if cmd != "" { - builder.AppendLoggable(cmd) - } - return builder -} From 550d124c4d159e0009b2c6bca5b05c54e8368c13 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 16 Feb 2024 20:40:54 -0800 Subject: [PATCH 08/22] Add pkg/kopia/cli package Signed-off-by: pavel.larkin --- pkg/kopia/cli/doc.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 pkg/kopia/cli/doc.go diff --git a/pkg/kopia/cli/doc.go b/pkg/kopia/cli/doc.go new file mode 100644 index 0000000000..6f0681452f --- /dev/null +++ b/pkg/kopia/cli/doc.go @@ -0,0 +1,21 @@ +package cli + +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ( + _ "github.com/kanisterio/safecli" +) + +// This package contains the implementation of the Kopia CLI using github.com/kanisterio/safecli. From fc918a0846cd6f4594da3536d0cd8982e84f2161 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 16 Feb 2024 20:41:22 -0800 Subject: [PATCH 09/22] go mod tidy Signed-off-by: pavel.larkin --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 233bc702f1..9e093b449a 100644 --- a/go.sum +++ b/go.sum @@ -362,8 +362,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kanisterio/safecli v0.0.3 h1:ts3oRVSoRexFBv9pOdWYZsN4k068pWx5Cl4zN54mQro= -github.com/kanisterio/safecli v0.0.3/go.mod h1:fK3Mcbeiso+NtkUdhGugK0Vf4S2l8ObvFf564ry1W5A= github.com/kanisterio/safecli v0.0.4 h1:8pO7Zhm9YeW47XQZNRt/AaXj7kSPeIbaV7aRDRtHs4k= github.com/kanisterio/safecli v0.0.4/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= From 2c6cb6e1b3dadf3a80e3355f9900ba3eddb23771 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Mon, 26 Feb 2024 15:38:03 -0800 Subject: [PATCH 10/22] Convert common flags from vars to funcs Signed-off-by: pavel.larkin --- pkg/kopia/cli/internal/opts/opts.go | 19 ++++++++++++++----- pkg/kopia/cli/internal/opts/opts_test.go | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/pkg/kopia/cli/internal/opts/opts.go b/pkg/kopia/cli/internal/opts/opts.go index bee0ece9d1..fb53f5d679 100644 --- a/pkg/kopia/cli/internal/opts/opts.go +++ b/pkg/kopia/cli/internal/opts/opts.go @@ -16,11 +16,20 @@ package opts import "github.com/kanisterio/safecli/command" -var ( - All = command.NewOption("--all", true) - Delta = command.NewOption("--delta", true) - ShowIdentical = command.NewOption("--show-identical", true) -) +// All creates a new all option. +func All(enabled bool) command.Applier { + return command.NewOption("--all", enabled) +} + +// Delta creates a new delta option. +func Delta(enabled bool) command.Applier { + return command.NewOption("--delta", enabled) +} + +// ShowIdentical creates a new show identical option. +func ShowIdentical(enabled bool) command.Applier { + return command.NewOption("--show-identical", enabled) +} // ReadOnly creates a new read only option. func ReadOnly(enabled bool) command.Applier { diff --git a/pkg/kopia/cli/internal/opts/opts_test.go b/pkg/kopia/cli/internal/opts/opts_test.go index 8ea81c5e25..f7aa7178ee 100644 --- a/pkg/kopia/cli/internal/opts/opts_test.go +++ b/pkg/kopia/cli/internal/opts/opts_test.go @@ -26,6 +26,21 @@ import ( func TestOptions(t *testing.T) { check.TestingT(t) } var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "All", + Argument: command.NewArguments(opts.All(true), opts.All(false)), + ExpectedCLI: []string{"cmd", "--all"}, + }, + { + Name: "Delta", + Argument: command.NewArguments(opts.Delta(true), opts.Delta(false)), + ExpectedCLI: []string{"cmd", "--delta"}, + }, + { + Name: "ShowIdentical", + Argument: command.NewArguments(opts.ShowIdentical(true), opts.ShowIdentical(false)), + ExpectedCLI: []string{"cmd", "--show-identical"}, + }, { Name: "Readonly", Argument: command.NewArguments(opts.ReadOnly(true), opts.ReadOnly(false)), From 1a3ee2df4c0422ab5c87e742c5c804d2fae495dd Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 2 Feb 2024 18:08:02 -0800 Subject: [PATCH 11/22] Add safecli dependency --- go.mod | 2 ++ go.sum | 2 ++ 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index 629fe7c11c..b00d7b6b8a 100644 --- a/go.mod +++ b/go.mod @@ -216,6 +216,8 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) +require github.com/kanisterio/safecli v0.0.3 + require ( github.com/Azure/go-autorest/autorest v0.11.27 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect diff --git a/go.sum b/go.sum index 8e946fc5ff..69a0e01e4b 100644 --- a/go.sum +++ b/go.sum @@ -359,6 +359,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kanisterio/safecli v0.0.3 h1:ts3oRVSoRexFBv9pOdWYZsN4k068pWx5Cl4zN54mQro= +github.com/kanisterio/safecli v0.0.3/go.mod h1:fK3Mcbeiso+NtkUdhGugK0Vf4S2l8ObvFf564ry1W5A= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734/go.mod h1:rdqSnvOJuKCPFW/h2rVLuXOAkRnHHdp9PZcKx4HCoDM= github.com/kastenhq/stow v0.2.6-kasten.1.0.20231101232131-9321daa23aae h1:2cl4yuAJpdmLCx7G8eIsfNlQBLEfw0JDj6mTTyqc5qg= From 386e7e942d477e80d19d6a33081c883c88142b0e Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 2 Feb 2024 18:09:47 -0800 Subject: [PATCH 12/22] add new flag implementations based on the safecli package for the Kopia CLI --- pkg/kopia/cli/errors.go | 25 +++ pkg/kopia/cli/internal/flag/bool_flag.go | 45 ++++++ pkg/kopia/cli/internal/flag/flag.go | 81 ++++++++++ pkg/kopia/cli/internal/flag/flag_test.go | 171 +++++++++++++++++++++ pkg/kopia/cli/internal/flag/string_flag.go | 78 ++++++++++ pkg/kopia/cli/internal/test/flag_suite.go | 112 ++++++++++++++ 6 files changed, 512 insertions(+) create mode 100644 pkg/kopia/cli/errors.go create mode 100644 pkg/kopia/cli/internal/flag/bool_flag.go create mode 100644 pkg/kopia/cli/internal/flag/flag.go create mode 100644 pkg/kopia/cli/internal/flag/flag_test.go create mode 100644 pkg/kopia/cli/internal/flag/string_flag.go create mode 100644 pkg/kopia/cli/internal/test/flag_suite.go diff --git a/pkg/kopia/cli/errors.go b/pkg/kopia/cli/errors.go new file mode 100644 index 0000000000..946dd04e87 --- /dev/null +++ b/pkg/kopia/cli/errors.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "github.com/pkg/errors" +) + +// flag errors +var ( + // ErrInvalidFlag is returned when the flag name is empty. + ErrInvalidFlag = errors.New("invalid flag") +) diff --git a/pkg/kopia/cli/internal/flag/bool_flag.go b/pkg/kopia/cli/internal/flag/bool_flag.go new file mode 100644 index 0000000000..4eea665cb2 --- /dev/null +++ b/pkg/kopia/cli/internal/flag/bool_flag.go @@ -0,0 +1,45 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag + +import ( + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli" +) + +// boolFlag defines a boolean flag with a given flag name. +// If enabled is set to true, the flag is applied; otherwise, it is not. +type boolFlag struct { + flag string + enabled bool +} + +// Apply appends the flag to the command if the flag is enabled. +func (f boolFlag) Apply(cli safecli.CommandAppender) error { + if f.enabled { + cli.AppendLoggable(f.flag) + } + return nil +} + +// NewBoolFlag creates a new bool flag with a given flag name. +// If the flag name is empty, cli.ErrInvalidFlag is returned. +func NewBoolFlag(flag string, enabled bool) Applier { + if flag == "" { + return ErrorFlag(cli.ErrInvalidFlag) + } + return boolFlag{flag, enabled} +} diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go new file mode 100644 index 0000000000..898ae31e16 --- /dev/null +++ b/pkg/kopia/cli/internal/flag/flag.go @@ -0,0 +1,81 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag + +import ( + "github.com/kanisterio/safecli" +) + +// Applier applies flags/args to the command. +type Applier interface { + // Apply applies the flags/args to the command. + Apply(cli safecli.CommandAppender) error +} + +// Apply appends multiple flags to the CLI. +// If any of the flags encounter an error during the Apply process, +// the error is returned and no changes are made to the CLI. +// If no error, the flags are appended to the CLI. +func Apply(cli safecli.CommandAppender, flags ...Applier) error { + // create a new sub builder which will be used to apply the flags + // to avoid mutating the CLI if an error is encountered. + sub := safecli.NewBuilder() + for _, flag := range flags { + if flag == nil { // if the flag is nil, skip it + continue + } + if err := flag.Apply(cli); err != nil { + return err + } + } + cli.Append(sub) + return nil +} + +// flags defines a collection of Flags. +type flags []Applier + +// Apply applies the flags to the CLI. +func (flags flags) Apply(cli safecli.CommandAppender) error { + return Apply(cli, flags...) +} + +// NewFlags creates a new collection of flags. +func NewFlags(fs ...Applier) Applier { + return flags(fs) +} + +// simpleFlag is a simple implementation of the Applier interface. +type simpleFlag struct { + err error +} + +// Apply does nothing except return an error if one is set. +func (f simpleFlag) Apply(safecli.CommandAppender) error { + return f.err +} + +// EmptyFlag creates a new flag that does nothing. +// It is useful for creating a no-op flag when a condition is not met +// but Applier interface is required. +func EmptyFlag() Applier { + return simpleFlag{} +} + +// ErrorFlag creates a new flag that returns an error when applied. +// It is useful for creating a flag validation if a condition is not met. +func ErrorFlag(err error) Applier { + return simpleFlag{err} +} diff --git a/pkg/kopia/cli/internal/flag/flag_test.go b/pkg/kopia/cli/internal/flag/flag_test.go new file mode 100644 index 0000000000..c20a46b450 --- /dev/null +++ b/pkg/kopia/cli/internal/flag/flag_test.go @@ -0,0 +1,171 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag_test + +import ( + "errors" + "testing" + + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/test" +) + +var ( + ErrFlag = errors.New("flag error") +) + +// MockFlagApplier is a mock implementation of the FlagApplier interface. +type MockFlagApplier struct { + flagName string + applyErr error +} + +func (m *MockFlagApplier) Apply(cli safecli.CommandAppender) error { + cli.AppendLoggable(m.flagName) + return m.applyErr +} + +func TestApply(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.FlagSuite{Cmd: "cmd", Tests: []test.FlagTest{ + { + Name: "Apply with no flags should generate only the command", + ExpectedCLI: []string{"cmd"}, + }, + { + Name: "Apply with nil flags should generate only the command", + Flag: flag.NewFlags(nil, nil), + ExpectedCLI: []string{"cmd"}, + }, + { + Name: "Apply with flags should generate the command and flags", + Flag: flag.NewFlags( + &MockFlagApplier{flagName: "--flag1", applyErr: nil}, + &MockFlagApplier{flagName: "--flag2", applyErr: nil}, + ), + ExpectedCLI: []string{"cmd", "--flag1", "--flag2"}, + }, + { + Name: "Apply with one error flag should not modify the command and return the error", + Flag: flag.NewFlags( + &MockFlagApplier{flagName: "flag1", applyErr: nil}, + &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: ErrFlag, + }, + { + Name: "NewBoolFlag", + Flag: flag.NewFlags( + flag.NewBoolFlag("--flag1", true), + flag.NewBoolFlag("--flag2", false), + ), + ExpectedCLI: []string{"cmd", "--flag1"}, + }, + { + Name: "NewBoolFlag with empty flag name should return an error", + Flag: flag.NewFlags( + flag.NewBoolFlag("", true), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewStringFlag", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + flag.NewStringFlag("--flag2", ""), + ), + ExpectedCLI: []string{"cmd", "--flag1=value1"}, + }, + { + Name: "NewStringFlag with all empty values should return an error", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + flag.NewStringFlag("", ""), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewRedactedStringFlag", + Flag: flag.NewFlags( + flag.NewRedactedStringFlag("--flag1", "value1"), + flag.NewRedactedStringFlag("--flag2", ""), + flag.NewRedactedStringFlag("", "value3"), + ), + ExpectedCLI: []string{"cmd", "--flag1=value1", "value3"}, + ExpectedLog: "cmd --flag1=<****> <****>", + }, + { + Name: "NewRedactedStringFlag with all empty values should return an error", + Flag: flag.NewFlags( + flag.NewRedactedStringFlag("--flag1", "value1"), + flag.NewRedactedStringFlag("", ""), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewStringValue", + Flag: flag.NewFlags( + flag.NewStringArgument("value1"), + ), + ExpectedCLI: []string{"cmd", "value1"}, + }, + { + Name: "NewStringValue with empty value should return an error", + Flag: flag.NewFlags( + flag.NewStringArgument(""), + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: cli.ErrInvalidFlag, + }, + { + Name: "NewFlags should generate multiple flags", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + flag.NewRedactedStringFlag("--flag2", "value2"), + flag.NewStringArgument("value3"), + ), + ExpectedCLI: []string{"cmd", "--flag1=value1", "--flag2=value2", "value3"}, + ExpectedLog: "cmd --flag1=value1 --flag2=<****> value3", + }, + { + Name: "NewFlags should generate no flags if one of them returns an error", + Flag: flag.NewFlags( + flag.NewStringFlag("--flag1", "value1"), + &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, + ), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: ErrFlag, + }, + { + Name: "EmptyFlag should not generate any flags", + Flag: flag.EmptyFlag(), + ExpectedCLI: []string{"cmd"}, + }, + { + Name: "ErrorFlag should return an error", + Flag: flag.ErrorFlag(ErrFlag), + ExpectedCLI: []string{"cmd"}, + ExpectedErr: ErrFlag, + }, +}}) diff --git a/pkg/kopia/cli/internal/flag/string_flag.go b/pkg/kopia/cli/internal/flag/string_flag.go new file mode 100644 index 0000000000..a94c29722c --- /dev/null +++ b/pkg/kopia/cli/internal/flag/string_flag.go @@ -0,0 +1,78 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flag + +import ( + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli" +) + +// stringFlag defines a string flag with a given flag name and value. +// If the value is empty, the flag is not applied. +type stringFlag struct { + flag string // flag name + value string // flag value + redacted bool // output the value as redacted +} + +// appenderFunc is a function that appends strings to a command. +type appenderFunc func(...string) *safecli.Builder + +// Apply appends the flag to the command if the value is not empty. +// If the value is redacted, it is appended as redacted. +func (f stringFlag) Apply(cli safecli.CommandAppender) error { + if f.value == "" { + return nil + } + appendValue, appendFlagValue := f.selectAppenderFuncs(cli) + if f.flag == "" { + appendValue(f.value) + } else { + appendFlagValue(f.flag, f.value) + } + return nil +} + +// selectAppenderFuncs returns the appropriate appender functions based on the redacted flag. +func (f stringFlag) selectAppenderFuncs(cli safecli.CommandAppender) (appenderFunc, appenderFunc) { + if f.redacted { + return cli.AppendRedacted, cli.AppendRedactedKV + } + return cli.AppendLoggable, cli.AppendLoggableKV +} + +// newStringFlag creates a new string flag with a given flag name and value. +func newStringFlag(flag, val string, redacted bool) Applier { + if flag == "" && val == "" { + return ErrorFlag(cli.ErrInvalidFlag) + } + return stringFlag{flag: flag, value: val, redacted: redacted} +} + +// NewStringFlag creates a new string flag with a given flag name and value. +func NewStringFlag(flag, val string) Applier { + return newStringFlag(flag, val, false) +} + +// NewRedactedStringFlag creates a new string flag with a given flag name and value. +func NewRedactedStringFlag(flag, val string) Applier { + return newStringFlag(flag, val, true) +} + +// NewStringArgument creates a new string argument with a given value. +func NewStringArgument(val string) Applier { + return newStringFlag("", val, false) +} diff --git a/pkg/kopia/cli/internal/test/flag_suite.go b/pkg/kopia/cli/internal/test/flag_suite.go new file mode 100644 index 0000000000..7c41a1ea3a --- /dev/null +++ b/pkg/kopia/cli/internal/test/flag_suite.go @@ -0,0 +1,112 @@ +package test + +import ( + "strings" + + "gopkg.in/check.v1" + + "github.com/pkg/errors" + + "github.com/kanisterio/safecli" + + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" +) + +// FlagTest defines a single test for a flag. +type FlagTest struct { + // Name of the test. (required) + Name string + + // Flag to test. (required) + Flag flag.Applier + + // Expected CLI arguments. (optional) + ExpectedCLI []string + + // Expected log output. (optional) + // if empty, it will be set to ExpectedCLI joined with space. + // if empty and ExpectedCLI is empty, it will be ignored. + ExpectedLog string + + // Expected error. (optional) + // If nil, no error is expected and + // ExpectedCLI and ExpectedLog are checked. + ExpectedErr error +} + +// CheckCommentString implements check.CommentInterface +func (t *FlagTest) CheckCommentString() string { + return t.Name +} + +// setDefaultExpectedLog sets the default value for ExpectedLog based on ExpectedCLI. +func (t *FlagTest) setDefaultExpectedLog() { + if len(t.ExpectedLog) == 0 && len(t.ExpectedCLI) > 0 { + t.ExpectedLog = strings.Join(t.ExpectedCLI, " ") + } +} + +// assertError checks the error against ExpectedErr. +func (t *FlagTest) assertError(c *check.C, err error) { + if actualErr := errors.Cause(err); actualErr != nil { + c.Assert(actualErr, check.Equals, t.ExpectedErr, t) + } else { + c.Assert(err, check.Equals, t.ExpectedErr, t) + } +} + +// assertNoError makes sure there is no error. +func (t *FlagTest) assertNoError(c *check.C, err error) { + c.Assert(err, check.IsNil, t) +} + +// assertCLI asserts the builder's CLI output against ExpectedCLI. +func (t *FlagTest) assertCLI(c *check.C, b *safecli.Builder) { + c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) +} + +// assertLog asserts the builder's log output against ExpectedLog. +func (t *FlagTest) assertLog(c *check.C, b *safecli.Builder) { + t.setDefaultExpectedLog() + c.Check(b.String(), check.Equals, t.ExpectedLog, t) +} + +// Test runs the flag test. +func (ft *FlagTest) Test(c *check.C, b *safecli.Builder) { + err := flag.Apply(b, ft.Flag) + if ft.ExpectedErr != nil { + ft.assertError(c, err) + } else { + ft.assertNoError(c, err) + ft.assertCLI(c, b) + ft.assertLog(c, b) + } +} + +// FlagSuite defines a test suite for flags. +type FlagSuite struct { + Cmd string // Cmd appends to the safecli.Builder before test if not empty. + Tests []FlagTest // Tests to run. +} + +// TestFlags runs all tests in the flag suite. +func (s *FlagSuite) TestFlags(c *check.C) { + for _, test := range s.Tests { + b := newBuilder(s.Cmd) + test.Test(c, b) + } +} + +// NewFlagSuite creates a new FlagSuite. +func NewFlagSuite(tests []FlagTest) *FlagSuite { + return &FlagSuite{Tests: tests} +} + +// newBuilder creates a new safecli.Builder with the given command. +func newBuilder(cmd string) *safecli.Builder { + builder := safecli.NewBuilder() + if cmd != "" { + builder.AppendLoggable(cmd) + } + return builder +} From b815f633208dd54aab0963b69d2298263190d4a6 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Sat, 3 Feb 2024 16:03:14 -0800 Subject: [PATCH 13/22] apply go fmt Signed-off-by: pavel.larkin --- pkg/kopia/cli/internal/flag/flag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go index 898ae31e16..66148034a1 100644 --- a/pkg/kopia/cli/internal/flag/flag.go +++ b/pkg/kopia/cli/internal/flag/flag.go @@ -68,7 +68,7 @@ func (f simpleFlag) Apply(safecli.CommandAppender) error { } // EmptyFlag creates a new flag that does nothing. -// It is useful for creating a no-op flag when a condition is not met +// It is useful for creating a no-op flag when a condition is not met // but Applier interface is required. func EmptyFlag() Applier { return simpleFlag{} From bcafabb2ba3c8c1cc71cb31c8417728cdd3e4610 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Mon, 12 Feb 2024 12:11:54 -0800 Subject: [PATCH 14/22] Fix Apply and test.Suit Signed-off-by: pavel.larkin --- pkg/kopia/cli/internal/flag/flag.go | 2 +- pkg/kopia/cli/internal/test/flag_suite.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go index 66148034a1..39a357be53 100644 --- a/pkg/kopia/cli/internal/flag/flag.go +++ b/pkg/kopia/cli/internal/flag/flag.go @@ -36,7 +36,7 @@ func Apply(cli safecli.CommandAppender, flags ...Applier) error { if flag == nil { // if the flag is nil, skip it continue } - if err := flag.Apply(cli); err != nil { + if err := flag.Apply(sub); err != nil { return err } } diff --git a/pkg/kopia/cli/internal/test/flag_suite.go b/pkg/kopia/cli/internal/test/flag_suite.go index 7c41a1ea3a..7fe60d909c 100644 --- a/pkg/kopia/cli/internal/test/flag_suite.go +++ b/pkg/kopia/cli/internal/test/flag_suite.go @@ -62,7 +62,9 @@ func (t *FlagTest) assertNoError(c *check.C, err error) { // assertCLI asserts the builder's CLI output against ExpectedCLI. func (t *FlagTest) assertCLI(c *check.C, b *safecli.Builder) { - c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) + if t.ExpectedCLI != nil { + c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) + } } // assertLog asserts the builder's log output against ExpectedLog. @@ -74,11 +76,11 @@ func (t *FlagTest) assertLog(c *check.C, b *safecli.Builder) { // Test runs the flag test. func (ft *FlagTest) Test(c *check.C, b *safecli.Builder) { err := flag.Apply(b, ft.Flag) + ft.assertCLI(c, b) if ft.ExpectedErr != nil { ft.assertError(c, err) } else { ft.assertNoError(c, err) - ft.assertCLI(c, b) ft.assertLog(c, b) } } From 6d61bb4c3e5a533f98bcea127655c7b5eba6e677 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 16 Feb 2024 16:51:58 -0800 Subject: [PATCH 15/22] pkg/kopia/cli/internal/flag is implemented in the safecli@v0.0.4 now Signed-off-by: pavel.larkin --- go.mod | 2 +- go.sum | 2 + pkg/kopia/cli/errors.go | 25 --- pkg/kopia/cli/internal/flag/bool_flag.go | 45 ------ pkg/kopia/cli/internal/flag/flag.go | 81 ---------- pkg/kopia/cli/internal/flag/flag_test.go | 171 --------------------- pkg/kopia/cli/internal/flag/string_flag.go | 78 ---------- pkg/kopia/cli/internal/test/flag_suite.go | 114 -------------- 8 files changed, 3 insertions(+), 515 deletions(-) delete mode 100644 pkg/kopia/cli/errors.go delete mode 100644 pkg/kopia/cli/internal/flag/bool_flag.go delete mode 100644 pkg/kopia/cli/internal/flag/flag.go delete mode 100644 pkg/kopia/cli/internal/flag/flag_test.go delete mode 100644 pkg/kopia/cli/internal/flag/string_flag.go delete mode 100644 pkg/kopia/cli/internal/test/flag_suite.go diff --git a/go.mod b/go.mod index b00d7b6b8a..fb6a6abd8d 100644 --- a/go.mod +++ b/go.mod @@ -216,7 +216,7 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) -require github.com/kanisterio/safecli v0.0.3 +require github.com/kanisterio/safecli v0.0.4 require ( github.com/Azure/go-autorest/autorest v0.11.27 // indirect diff --git a/go.sum b/go.sum index 69a0e01e4b..5ebbafea17 100644 --- a/go.sum +++ b/go.sum @@ -361,6 +361,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kanisterio/safecli v0.0.3 h1:ts3oRVSoRexFBv9pOdWYZsN4k068pWx5Cl4zN54mQro= github.com/kanisterio/safecli v0.0.3/go.mod h1:fK3Mcbeiso+NtkUdhGugK0Vf4S2l8ObvFf564ry1W5A= +github.com/kanisterio/safecli v0.0.4 h1:8pO7Zhm9YeW47XQZNRt/AaXj7kSPeIbaV7aRDRtHs4k= +github.com/kanisterio/safecli v0.0.4/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734/go.mod h1:rdqSnvOJuKCPFW/h2rVLuXOAkRnHHdp9PZcKx4HCoDM= github.com/kastenhq/stow v0.2.6-kasten.1.0.20231101232131-9321daa23aae h1:2cl4yuAJpdmLCx7G8eIsfNlQBLEfw0JDj6mTTyqc5qg= diff --git a/pkg/kopia/cli/errors.go b/pkg/kopia/cli/errors.go deleted file mode 100644 index 946dd04e87..0000000000 --- a/pkg/kopia/cli/errors.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cli - -import ( - "github.com/pkg/errors" -) - -// flag errors -var ( - // ErrInvalidFlag is returned when the flag name is empty. - ErrInvalidFlag = errors.New("invalid flag") -) diff --git a/pkg/kopia/cli/internal/flag/bool_flag.go b/pkg/kopia/cli/internal/flag/bool_flag.go deleted file mode 100644 index 4eea665cb2..0000000000 --- a/pkg/kopia/cli/internal/flag/bool_flag.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag - -import ( - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli" -) - -// boolFlag defines a boolean flag with a given flag name. -// If enabled is set to true, the flag is applied; otherwise, it is not. -type boolFlag struct { - flag string - enabled bool -} - -// Apply appends the flag to the command if the flag is enabled. -func (f boolFlag) Apply(cli safecli.CommandAppender) error { - if f.enabled { - cli.AppendLoggable(f.flag) - } - return nil -} - -// NewBoolFlag creates a new bool flag with a given flag name. -// If the flag name is empty, cli.ErrInvalidFlag is returned. -func NewBoolFlag(flag string, enabled bool) Applier { - if flag == "" { - return ErrorFlag(cli.ErrInvalidFlag) - } - return boolFlag{flag, enabled} -} diff --git a/pkg/kopia/cli/internal/flag/flag.go b/pkg/kopia/cli/internal/flag/flag.go deleted file mode 100644 index 39a357be53..0000000000 --- a/pkg/kopia/cli/internal/flag/flag.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag - -import ( - "github.com/kanisterio/safecli" -) - -// Applier applies flags/args to the command. -type Applier interface { - // Apply applies the flags/args to the command. - Apply(cli safecli.CommandAppender) error -} - -// Apply appends multiple flags to the CLI. -// If any of the flags encounter an error during the Apply process, -// the error is returned and no changes are made to the CLI. -// If no error, the flags are appended to the CLI. -func Apply(cli safecli.CommandAppender, flags ...Applier) error { - // create a new sub builder which will be used to apply the flags - // to avoid mutating the CLI if an error is encountered. - sub := safecli.NewBuilder() - for _, flag := range flags { - if flag == nil { // if the flag is nil, skip it - continue - } - if err := flag.Apply(sub); err != nil { - return err - } - } - cli.Append(sub) - return nil -} - -// flags defines a collection of Flags. -type flags []Applier - -// Apply applies the flags to the CLI. -func (flags flags) Apply(cli safecli.CommandAppender) error { - return Apply(cli, flags...) -} - -// NewFlags creates a new collection of flags. -func NewFlags(fs ...Applier) Applier { - return flags(fs) -} - -// simpleFlag is a simple implementation of the Applier interface. -type simpleFlag struct { - err error -} - -// Apply does nothing except return an error if one is set. -func (f simpleFlag) Apply(safecli.CommandAppender) error { - return f.err -} - -// EmptyFlag creates a new flag that does nothing. -// It is useful for creating a no-op flag when a condition is not met -// but Applier interface is required. -func EmptyFlag() Applier { - return simpleFlag{} -} - -// ErrorFlag creates a new flag that returns an error when applied. -// It is useful for creating a flag validation if a condition is not met. -func ErrorFlag(err error) Applier { - return simpleFlag{err} -} diff --git a/pkg/kopia/cli/internal/flag/flag_test.go b/pkg/kopia/cli/internal/flag/flag_test.go deleted file mode 100644 index c20a46b450..0000000000 --- a/pkg/kopia/cli/internal/flag/flag_test.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag_test - -import ( - "errors" - "testing" - - "gopkg.in/check.v1" - - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/test" -) - -var ( - ErrFlag = errors.New("flag error") -) - -// MockFlagApplier is a mock implementation of the FlagApplier interface. -type MockFlagApplier struct { - flagName string - applyErr error -} - -func (m *MockFlagApplier) Apply(cli safecli.CommandAppender) error { - cli.AppendLoggable(m.flagName) - return m.applyErr -} - -func TestApply(t *testing.T) { check.TestingT(t) } - -var _ = check.Suite(&test.FlagSuite{Cmd: "cmd", Tests: []test.FlagTest{ - { - Name: "Apply with no flags should generate only the command", - ExpectedCLI: []string{"cmd"}, - }, - { - Name: "Apply with nil flags should generate only the command", - Flag: flag.NewFlags(nil, nil), - ExpectedCLI: []string{"cmd"}, - }, - { - Name: "Apply with flags should generate the command and flags", - Flag: flag.NewFlags( - &MockFlagApplier{flagName: "--flag1", applyErr: nil}, - &MockFlagApplier{flagName: "--flag2", applyErr: nil}, - ), - ExpectedCLI: []string{"cmd", "--flag1", "--flag2"}, - }, - { - Name: "Apply with one error flag should not modify the command and return the error", - Flag: flag.NewFlags( - &MockFlagApplier{flagName: "flag1", applyErr: nil}, - &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: ErrFlag, - }, - { - Name: "NewBoolFlag", - Flag: flag.NewFlags( - flag.NewBoolFlag("--flag1", true), - flag.NewBoolFlag("--flag2", false), - ), - ExpectedCLI: []string{"cmd", "--flag1"}, - }, - { - Name: "NewBoolFlag with empty flag name should return an error", - Flag: flag.NewFlags( - flag.NewBoolFlag("", true), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewStringFlag", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - flag.NewStringFlag("--flag2", ""), - ), - ExpectedCLI: []string{"cmd", "--flag1=value1"}, - }, - { - Name: "NewStringFlag with all empty values should return an error", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - flag.NewStringFlag("", ""), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewRedactedStringFlag", - Flag: flag.NewFlags( - flag.NewRedactedStringFlag("--flag1", "value1"), - flag.NewRedactedStringFlag("--flag2", ""), - flag.NewRedactedStringFlag("", "value3"), - ), - ExpectedCLI: []string{"cmd", "--flag1=value1", "value3"}, - ExpectedLog: "cmd --flag1=<****> <****>", - }, - { - Name: "NewRedactedStringFlag with all empty values should return an error", - Flag: flag.NewFlags( - flag.NewRedactedStringFlag("--flag1", "value1"), - flag.NewRedactedStringFlag("", ""), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewStringValue", - Flag: flag.NewFlags( - flag.NewStringArgument("value1"), - ), - ExpectedCLI: []string{"cmd", "value1"}, - }, - { - Name: "NewStringValue with empty value should return an error", - Flag: flag.NewFlags( - flag.NewStringArgument(""), - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: cli.ErrInvalidFlag, - }, - { - Name: "NewFlags should generate multiple flags", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - flag.NewRedactedStringFlag("--flag2", "value2"), - flag.NewStringArgument("value3"), - ), - ExpectedCLI: []string{"cmd", "--flag1=value1", "--flag2=value2", "value3"}, - ExpectedLog: "cmd --flag1=value1 --flag2=<****> value3", - }, - { - Name: "NewFlags should generate no flags if one of them returns an error", - Flag: flag.NewFlags( - flag.NewStringFlag("--flag1", "value1"), - &MockFlagApplier{flagName: "flag2", applyErr: ErrFlag}, - ), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: ErrFlag, - }, - { - Name: "EmptyFlag should not generate any flags", - Flag: flag.EmptyFlag(), - ExpectedCLI: []string{"cmd"}, - }, - { - Name: "ErrorFlag should return an error", - Flag: flag.ErrorFlag(ErrFlag), - ExpectedCLI: []string{"cmd"}, - ExpectedErr: ErrFlag, - }, -}}) diff --git a/pkg/kopia/cli/internal/flag/string_flag.go b/pkg/kopia/cli/internal/flag/string_flag.go deleted file mode 100644 index a94c29722c..0000000000 --- a/pkg/kopia/cli/internal/flag/string_flag.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2024 The Kanister Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flag - -import ( - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli" -) - -// stringFlag defines a string flag with a given flag name and value. -// If the value is empty, the flag is not applied. -type stringFlag struct { - flag string // flag name - value string // flag value - redacted bool // output the value as redacted -} - -// appenderFunc is a function that appends strings to a command. -type appenderFunc func(...string) *safecli.Builder - -// Apply appends the flag to the command if the value is not empty. -// If the value is redacted, it is appended as redacted. -func (f stringFlag) Apply(cli safecli.CommandAppender) error { - if f.value == "" { - return nil - } - appendValue, appendFlagValue := f.selectAppenderFuncs(cli) - if f.flag == "" { - appendValue(f.value) - } else { - appendFlagValue(f.flag, f.value) - } - return nil -} - -// selectAppenderFuncs returns the appropriate appender functions based on the redacted flag. -func (f stringFlag) selectAppenderFuncs(cli safecli.CommandAppender) (appenderFunc, appenderFunc) { - if f.redacted { - return cli.AppendRedacted, cli.AppendRedactedKV - } - return cli.AppendLoggable, cli.AppendLoggableKV -} - -// newStringFlag creates a new string flag with a given flag name and value. -func newStringFlag(flag, val string, redacted bool) Applier { - if flag == "" && val == "" { - return ErrorFlag(cli.ErrInvalidFlag) - } - return stringFlag{flag: flag, value: val, redacted: redacted} -} - -// NewStringFlag creates a new string flag with a given flag name and value. -func NewStringFlag(flag, val string) Applier { - return newStringFlag(flag, val, false) -} - -// NewRedactedStringFlag creates a new string flag with a given flag name and value. -func NewRedactedStringFlag(flag, val string) Applier { - return newStringFlag(flag, val, true) -} - -// NewStringArgument creates a new string argument with a given value. -func NewStringArgument(val string) Applier { - return newStringFlag("", val, false) -} diff --git a/pkg/kopia/cli/internal/test/flag_suite.go b/pkg/kopia/cli/internal/test/flag_suite.go deleted file mode 100644 index 7fe60d909c..0000000000 --- a/pkg/kopia/cli/internal/test/flag_suite.go +++ /dev/null @@ -1,114 +0,0 @@ -package test - -import ( - "strings" - - "gopkg.in/check.v1" - - "github.com/pkg/errors" - - "github.com/kanisterio/safecli" - - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/flag" -) - -// FlagTest defines a single test for a flag. -type FlagTest struct { - // Name of the test. (required) - Name string - - // Flag to test. (required) - Flag flag.Applier - - // Expected CLI arguments. (optional) - ExpectedCLI []string - - // Expected log output. (optional) - // if empty, it will be set to ExpectedCLI joined with space. - // if empty and ExpectedCLI is empty, it will be ignored. - ExpectedLog string - - // Expected error. (optional) - // If nil, no error is expected and - // ExpectedCLI and ExpectedLog are checked. - ExpectedErr error -} - -// CheckCommentString implements check.CommentInterface -func (t *FlagTest) CheckCommentString() string { - return t.Name -} - -// setDefaultExpectedLog sets the default value for ExpectedLog based on ExpectedCLI. -func (t *FlagTest) setDefaultExpectedLog() { - if len(t.ExpectedLog) == 0 && len(t.ExpectedCLI) > 0 { - t.ExpectedLog = strings.Join(t.ExpectedCLI, " ") - } -} - -// assertError checks the error against ExpectedErr. -func (t *FlagTest) assertError(c *check.C, err error) { - if actualErr := errors.Cause(err); actualErr != nil { - c.Assert(actualErr, check.Equals, t.ExpectedErr, t) - } else { - c.Assert(err, check.Equals, t.ExpectedErr, t) - } -} - -// assertNoError makes sure there is no error. -func (t *FlagTest) assertNoError(c *check.C, err error) { - c.Assert(err, check.IsNil, t) -} - -// assertCLI asserts the builder's CLI output against ExpectedCLI. -func (t *FlagTest) assertCLI(c *check.C, b *safecli.Builder) { - if t.ExpectedCLI != nil { - c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI, t) - } -} - -// assertLog asserts the builder's log output against ExpectedLog. -func (t *FlagTest) assertLog(c *check.C, b *safecli.Builder) { - t.setDefaultExpectedLog() - c.Check(b.String(), check.Equals, t.ExpectedLog, t) -} - -// Test runs the flag test. -func (ft *FlagTest) Test(c *check.C, b *safecli.Builder) { - err := flag.Apply(b, ft.Flag) - ft.assertCLI(c, b) - if ft.ExpectedErr != nil { - ft.assertError(c, err) - } else { - ft.assertNoError(c, err) - ft.assertLog(c, b) - } -} - -// FlagSuite defines a test suite for flags. -type FlagSuite struct { - Cmd string // Cmd appends to the safecli.Builder before test if not empty. - Tests []FlagTest // Tests to run. -} - -// TestFlags runs all tests in the flag suite. -func (s *FlagSuite) TestFlags(c *check.C) { - for _, test := range s.Tests { - b := newBuilder(s.Cmd) - test.Test(c, b) - } -} - -// NewFlagSuite creates a new FlagSuite. -func NewFlagSuite(tests []FlagTest) *FlagSuite { - return &FlagSuite{Tests: tests} -} - -// newBuilder creates a new safecli.Builder with the given command. -func newBuilder(cmd string) *safecli.Builder { - builder := safecli.NewBuilder() - if cmd != "" { - builder.AppendLoggable(cmd) - } - return builder -} From e6ddb8f6bef447dfed85029a4b6f4632c3d874d5 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 16 Feb 2024 20:40:54 -0800 Subject: [PATCH 16/22] Add pkg/kopia/cli package Signed-off-by: pavel.larkin --- pkg/kopia/cli/doc.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 pkg/kopia/cli/doc.go diff --git a/pkg/kopia/cli/doc.go b/pkg/kopia/cli/doc.go new file mode 100644 index 0000000000..6f0681452f --- /dev/null +++ b/pkg/kopia/cli/doc.go @@ -0,0 +1,21 @@ +package cli + +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ( + _ "github.com/kanisterio/safecli" +) + +// This package contains the implementation of the Kopia CLI using github.com/kanisterio/safecli. From 4e7ffd787fc799d5869d2cbd083bd2b8f591ee52 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Fri, 16 Feb 2024 20:41:22 -0800 Subject: [PATCH 17/22] go mod tidy Signed-off-by: pavel.larkin --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 5ebbafea17..67c3138454 100644 --- a/go.sum +++ b/go.sum @@ -359,8 +359,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kanisterio/safecli v0.0.3 h1:ts3oRVSoRexFBv9pOdWYZsN4k068pWx5Cl4zN54mQro= -github.com/kanisterio/safecli v0.0.3/go.mod h1:fK3Mcbeiso+NtkUdhGugK0Vf4S2l8ObvFf564ry1W5A= github.com/kanisterio/safecli v0.0.4 h1:8pO7Zhm9YeW47XQZNRt/AaXj7kSPeIbaV7aRDRtHs4k= github.com/kanisterio/safecli v0.0.4/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= From f16aea76d2e21eab94df31c1a689eeba7d57e375 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Tue, 27 Feb 2024 13:33:47 -0800 Subject: [PATCH 18/22] Update safecli to v0.0.5 Signed-off-by: pavel.larkin --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fb6a6abd8d..95b851aa8f 100644 --- a/go.mod +++ b/go.mod @@ -216,7 +216,7 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) -require github.com/kanisterio/safecli v0.0.4 +require github.com/kanisterio/safecli v0.0.5 require ( github.com/Azure/go-autorest/autorest v0.11.27 // indirect diff --git a/go.sum b/go.sum index 67c3138454..26b154c2a6 100644 --- a/go.sum +++ b/go.sum @@ -359,8 +359,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kanisterio/safecli v0.0.4 h1:8pO7Zhm9YeW47XQZNRt/AaXj7kSPeIbaV7aRDRtHs4k= -github.com/kanisterio/safecli v0.0.4/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= +github.com/kanisterio/safecli v0.0.5 h1:1B9JkmmE4YYCIj4eYMVUXJXQpbpQ+GSOrGoqacfi58Q= +github.com/kanisterio/safecli v0.0.5/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734/go.mod h1:rdqSnvOJuKCPFW/h2rVLuXOAkRnHHdp9PZcKx4HCoDM= github.com/kastenhq/stow v0.2.6-kasten.1.0.20231101232131-9321daa23aae h1:2cl4yuAJpdmLCx7G8eIsfNlQBLEfw0JDj6mTTyqc5qg= From dcd6425038f059ea148871fcd8ab1e44b754d417 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Tue, 27 Feb 2024 16:25:09 -0800 Subject: [PATCH 19/22] Update safecli to v0.0.6 Signed-off-by: pavel.larkin --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 95b851aa8f..f655f747a9 100644 --- a/go.mod +++ b/go.mod @@ -216,7 +216,7 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) -require github.com/kanisterio/safecli v0.0.5 +require github.com/kanisterio/safecli v0.0.6 require ( github.com/Azure/go-autorest/autorest v0.11.27 // indirect diff --git a/go.sum b/go.sum index 26b154c2a6..0d88ed3030 100644 --- a/go.sum +++ b/go.sum @@ -359,8 +359,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kanisterio/safecli v0.0.5 h1:1B9JkmmE4YYCIj4eYMVUXJXQpbpQ+GSOrGoqacfi58Q= -github.com/kanisterio/safecli v0.0.5/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= +github.com/kanisterio/safecli v0.0.6 h1:Mq99jK7A/SBiHKZalUIlsk4qaSVZJNbBWb8rjaJv/Jk= +github.com/kanisterio/safecli v0.0.6/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734 h1:qulsCaCv+O2y9/sQ9nd5KChnAgFOWakTHQ9ZADjs6DQ= github.com/kastenhq/check v0.0.0-20180626002341-0264cfcea734/go.mod h1:rdqSnvOJuKCPFW/h2rVLuXOAkRnHHdp9PZcKx4HCoDM= github.com/kastenhq/stow v0.2.6-kasten.1.0.20231101232131-9321daa23aae h1:2cl4yuAJpdmLCx7G8eIsfNlQBLEfw0JDj6mTTyqc5qg= From b093421406784745d822f1fc8c92402803230d25 Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Wed, 28 Feb 2024 14:58:18 -0800 Subject: [PATCH 20/22] Fix tests Signed-off-by: pavel.larkin --- pkg/kopia/cli/internal/opts/common_opts.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/kopia/cli/internal/opts/common_opts.go b/pkg/kopia/cli/internal/opts/common_opts.go index 4eb8b2b101..641de33528 100644 --- a/pkg/kopia/cli/internal/opts/common_opts.go +++ b/pkg/kopia/cli/internal/opts/common_opts.go @@ -24,7 +24,11 @@ const ( ) // LogDirectory creates a new log directory option with a given directory. +// if the directory is empty, the log directory option is not set. func LogDirectory(dir string) command.Applier { + if dir == "" { + return command.NewNoopArgument() + } return command.NewOptionWithArgument("--log-dir", dir) } @@ -38,12 +42,20 @@ func LogLevel(level string) command.Applier { } // ConfigFilePath creates a new config file path option with a given path. +// If the path is empty, the config file path option is not set. func ConfigFilePath(path string) command.Applier { + if path == "" { + return command.NewNoopArgument() + } return command.NewOptionWithArgument("--config-file", path) } // RepoPassword creates a new repository password option with a given password. +// If the password is empty, the repository password option is not set. func RepoPassword(password string) command.Applier { + if password == "" { + return command.NewNoopArgument() + } return command.NewOptionWithRedactedArgument("--password", password) } From 44c133ec05dae55112f85ae614237cc19882092d Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Thu, 29 Feb 2024 13:54:31 -0800 Subject: [PATCH 21/22] Fix formatting Signed-off-by: pavel.larkin --- pkg/kopia/cli/doc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/kopia/cli/doc.go b/pkg/kopia/cli/doc.go index 6f0681452f..75425c26e4 100644 --- a/pkg/kopia/cli/doc.go +++ b/pkg/kopia/cli/doc.go @@ -1,5 +1,3 @@ -package cli - // Copyright 2024 The Kanister Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +12,10 @@ package cli // See the License for the specific language governing permissions and // limitations under the License. +// This package contains the implementation of the Kopia CLI using github.com/kanisterio/safecli. + +package cli + import ( _ "github.com/kanisterio/safecli" ) - -// This package contains the implementation of the Kopia CLI using github.com/kanisterio/safecli. From 33b1342f23779161506b26d7f65cf81732cb4b7f Mon Sep 17 00:00:00 2001 From: "pavel.larkin" Date: Thu, 29 Feb 2024 15:16:35 -0800 Subject: [PATCH 22/22] organize imports Signed-off-by: pavel.larkin --- pkg/kopia/cli/internal/args/args.go | 3 ++- pkg/kopia/cli/internal/args/args_test.go | 5 +++-- pkg/kopia/cli/internal/opts/cache_opts.go | 3 ++- pkg/kopia/cli/internal/opts/cache_opts_test.go | 5 +++-- pkg/kopia/cli/internal/opts/common_opts.go | 3 ++- pkg/kopia/cli/internal/opts/common_opts_test.go | 5 +++-- pkg/kopia/cli/internal/opts/opts_test.go | 3 ++- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pkg/kopia/cli/internal/args/args.go b/pkg/kopia/cli/internal/args/args.go index a6a8223b6d..cad732dc9d 100644 --- a/pkg/kopia/cli/internal/args/args.go +++ b/pkg/kopia/cli/internal/args/args.go @@ -15,8 +15,9 @@ package args import ( - "github.com/kanisterio/kanister/pkg/kopia/cli" "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/kanister/pkg/kopia/cli" ) // ID creates a new ID argument. diff --git a/pkg/kopia/cli/internal/args/args_test.go b/pkg/kopia/cli/internal/args/args_test.go index ea6fac26ff..c8362009e8 100644 --- a/pkg/kopia/cli/internal/args/args_test.go +++ b/pkg/kopia/cli/internal/args/args_test.go @@ -17,10 +17,11 @@ package args_test import ( "testing" - "github.com/kanisterio/kanister/pkg/kopia/cli" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/args" "github.com/kanisterio/safecli/test" "gopkg.in/check.v1" + + "github.com/kanisterio/kanister/pkg/kopia/cli" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/args" ) func TestArgs(t *testing.T) { check.TestingT(t) } diff --git a/pkg/kopia/cli/internal/opts/cache_opts.go b/pkg/kopia/cli/internal/opts/cache_opts.go index 75fa0b8f89..fb1222b4af 100644 --- a/pkg/kopia/cli/internal/opts/cache_opts.go +++ b/pkg/kopia/cli/internal/opts/cache_opts.go @@ -17,8 +17,9 @@ package opts import ( "strconv" - "github.com/kanisterio/kanister/pkg/kopia/cli/args" "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/kanister/pkg/kopia/cli/args" ) const ( diff --git a/pkg/kopia/cli/internal/opts/cache_opts_test.go b/pkg/kopia/cli/internal/opts/cache_opts_test.go index 40c56d8966..ee87091144 100644 --- a/pkg/kopia/cli/internal/opts/cache_opts_test.go +++ b/pkg/kopia/cli/internal/opts/cache_opts_test.go @@ -17,11 +17,12 @@ package opts_test import ( "testing" - "github.com/kanisterio/kanister/pkg/kopia/cli/args" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/opts" "github.com/kanisterio/safecli/command" "github.com/kanisterio/safecli/test" "gopkg.in/check.v1" + + "github.com/kanisterio/kanister/pkg/kopia/cli/args" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/opts" ) func TestCacheOptions(t *testing.T) { check.TestingT(t) } diff --git a/pkg/kopia/cli/internal/opts/common_opts.go b/pkg/kopia/cli/internal/opts/common_opts.go index 641de33528..d17f71e5c3 100644 --- a/pkg/kopia/cli/internal/opts/common_opts.go +++ b/pkg/kopia/cli/internal/opts/common_opts.go @@ -15,8 +15,9 @@ package opts import ( - "github.com/kanisterio/kanister/pkg/kopia/cli/args" "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/kanister/pkg/kopia/cli/args" ) const ( diff --git a/pkg/kopia/cli/internal/opts/common_opts_test.go b/pkg/kopia/cli/internal/opts/common_opts_test.go index f3f24131a4..28560a9537 100644 --- a/pkg/kopia/cli/internal/opts/common_opts_test.go +++ b/pkg/kopia/cli/internal/opts/common_opts_test.go @@ -17,11 +17,12 @@ package opts_test import ( "testing" - "github.com/kanisterio/kanister/pkg/kopia/cli/args" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/opts" "github.com/kanisterio/safecli/command" "github.com/kanisterio/safecli/test" "gopkg.in/check.v1" + + "github.com/kanisterio/kanister/pkg/kopia/cli/args" + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/opts" ) func TestCommonOptions(t *testing.T) { check.TestingT(t) } diff --git a/pkg/kopia/cli/internal/opts/opts_test.go b/pkg/kopia/cli/internal/opts/opts_test.go index f7aa7178ee..c0e955fd85 100644 --- a/pkg/kopia/cli/internal/opts/opts_test.go +++ b/pkg/kopia/cli/internal/opts/opts_test.go @@ -17,10 +17,11 @@ package opts_test import ( "testing" - "github.com/kanisterio/kanister/pkg/kopia/cli/internal/opts" "github.com/kanisterio/safecli/command" "github.com/kanisterio/safecli/test" "gopkg.in/check.v1" + + "github.com/kanisterio/kanister/pkg/kopia/cli/internal/opts" ) func TestOptions(t *testing.T) { check.TestingT(t) }