Change logic of config set-context cmd

* command internal logic was revised and optimized
 * added ability to delete/reset context fields
 * unit tests were reorganized, new cases were added

Change-Id: Ie35d11405e88fea21abf33cb75f44b03bb4644fd
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Relates-To: #348
This commit is contained in:
Ruslan Aliev 2021-02-26 23:22:08 -06:00
parent b8e0978d6d
commit 3dd66f7685
12 changed files with 226 additions and 222 deletions

View File

@ -15,9 +15,8 @@ limitations under the License.
package config package config
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/config"
) )
@ -37,6 +36,10 @@ airshipctl config set-context \
--current \ --current \
--manifest=exampleManifest --manifest=exampleManifest
` `
setContextManifestFlag = "manifest"
setContextManagementConfigFlag = "management-config"
setContextCurrentFlag = "current"
) )
// NewSetContextCommand creates a command for creating and modifying contexts // NewSetContextCommand creates a command for creating and modifying contexts
@ -49,51 +52,60 @@ func NewSetContextCommand(cfgFactory config.Factory) *cobra.Command {
Long: setContextLong[1:], Long: setContextLong[1:],
Example: setContextExample, Example: setContextExample,
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: setContextRunE(cfgFactory, o),
nFlags := cmd.Flags().NFlag()
if len(args) == 1 {
// context name is made optional with --current flag added
o.Name = args[0]
}
if o.Name != "" && nFlags == 0 {
fmt.Fprintf(cmd.OutOrStdout(), "Context %q not modified. No new options provided.\n", o.Name)
return nil
}
cfg, err := cfgFactory()
if err != nil {
return err
}
modified, err := config.RunSetContext(o, cfg, true)
if err != nil {
return err
}
if modified {
fmt.Fprintf(cmd.OutOrStdout(), "Context %q modified.\n", o.Name)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "Context %q created.\n", o.Name)
}
return nil
},
} }
addSetContextFlags(o, cmd) addSetContextFlags(cmd, o)
return cmd return cmd
} }
func addSetContextFlags(o *config.ContextOptions, cmd *cobra.Command) { func addSetContextFlags(cmd *cobra.Command, o *config.ContextOptions) {
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar( flags.StringVar(
&o.Manifest, &o.Manifest,
"manifest", setContextManifestFlag,
"", "",
"set the manifest for the specified context") "set the manifest for the specified context")
flags.StringVar(
&o.ManagementConfiguration,
setContextManagementConfigFlag,
"",
"set the management config for the specified context")
flags.BoolVar( flags.BoolVar(
&o.Current, &o.Current,
"current", setContextCurrentFlag,
false, false,
"update the current context") "update the current context")
} }
func setContextRunE(cfgFactory config.Factory, o *config.ContextOptions) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctxName := ""
if len(args) == 1 {
ctxName = args[0]
}
// Go through all the flags that have been set
var opts []config.ContextOption
fn := func(flag *pflag.Flag) {
switch flag.Name {
case setContextManifestFlag:
opts = append(opts, config.SetContextManifest(o.Manifest))
case setContextManagementConfigFlag:
opts = append(opts, config.SetContextManagementConfig(o.ManagementConfiguration))
}
}
cmd.Flags().Visit(fn)
options := &config.RunSetContextOptions{
CfgFactory: cfgFactory,
CtxName: ctxName,
Current: o.Current,
Writer: cmd.OutOrStdout(),
}
return options.RunSetContext(opts...)
}
}

View File

@ -15,109 +15,21 @@ limitations under the License.
package config_test package config_test
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "opendev.org/airship/airshipctl/cmd/config"
"github.com/stretchr/testify/require"
cmd "opendev.org/airship/airshipctl/cmd/config"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/testutil" "opendev.org/airship/airshipctl/testutil"
) )
const (
defaultManifest = "edge_cloud"
)
type setContextTest struct {
givenConfig *config.Config
cmdTest *testutil.CmdTest
contextName string
manifest string
}
func TestConfigSetContext(t *testing.T) { func TestConfigSetContext(t *testing.T) {
cmdTests := []*testutil.CmdTest{ cmdTests := []*testutil.CmdTest{
{ {
Name: "config-cmd-set-context-with-help", Name: "config-cmd-set-context-with-help",
CmdLine: "--help", CmdLine: "--help",
Cmd: cmd.NewSetContextCommand(nil), Cmd: config.NewSetContextCommand(nil),
},
{
Name: "config-cmd-set-context-no-flags",
CmdLine: "context",
Cmd: cmd.NewSetContextCommand(nil),
},
{
Name: "config-cmd-set-context-too-many-args",
CmdLine: "arg1 arg2",
Cmd: cmd.NewSetContextCommand(nil),
Error: fmt.Errorf("accepts at most %d arg(s), received %d", 1, 2),
}, },
} }
for _, tt := range cmdTests { for _, tt := range cmdTests {
testutil.RunTest(t, tt) testutil.RunTest(t, tt)
} }
} }
func TestSetContext(t *testing.T) {
given, cleanupGiven := testutil.InitConfig(t)
defer cleanupGiven(t)
tests := []struct {
testName string
contextName string
flags []string
givenConfig *config.Config
manifest string
}{
{
testName: "set-context",
contextName: "dummycontext",
flags: []string{
"--manifest=" + defaultManifest,
},
givenConfig: given,
manifest: defaultManifest,
},
}
for _, tt := range tests {
tt := tt
cmd := &testutil.CmdTest{
Name: tt.testName,
CmdLine: fmt.Sprintf("%s %s", tt.contextName, strings.Join(tt.flags, " ")),
}
test := setContextTest{
givenConfig: tt.givenConfig,
cmdTest: cmd,
contextName: tt.contextName,
manifest: tt.manifest,
}
test.run(t)
}
}
func (test setContextTest) run(t *testing.T) {
// Get the Environment
settings := func() (*config.Config, error) {
return test.givenConfig, nil
}
test.cmdTest.Cmd = cmd.NewSetContextCommand(settings)
testutil.RunTest(t, test.cmdTest)
afterRunConf := test.givenConfig
// Find the Context Created or Modified
afterRunContext, err := afterRunConf.GetContext(test.contextName)
require.NoError(t, err)
require.NotNil(t, afterRunContext)
if test.manifest != "" {
assert.EqualValues(t, afterRunContext.Manifest, test.manifest)
}
}

View File

@ -1 +0,0 @@
Context "context" not modified. No new options provided.

View File

@ -1,21 +0,0 @@
Error: accepts at most 1 arg(s), received 2
Usage:
set-context NAME [flags]
Examples:
# Create a new context named "exampleContext"
airshipctl config set-context exampleContext \
--manifest=exampleManifest \
# Update the manifest of the current-context
airshipctl config set-context \
--current \
--manifest=exampleManifest
Flags:
--current update the current context
-h, --help help for set-context
--manifest string set the manifest for the specified context

View File

@ -16,6 +16,7 @@ airshipctl config set-context \
Flags: Flags:
--current update the current context --current update the current context
-h, --help help for set-context -h, --help help for set-context
--manifest string set the manifest for the specified context --management-config string set the management config for the specified context
--manifest string set the manifest for the specified context

View File

@ -1 +0,0 @@
Context "dummycontext" created.

View File

@ -29,9 +29,10 @@ airshipctl config set-context \
### Options ### Options
``` ```
--current update the current context --current update the current context
-h, --help help for set-context -h, --help help for set-context
--manifest string set the manifest for the specified context --management-config string set the management config for the specified context
--manifest string set the manifest for the specified context
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

1
go.mod
View File

@ -30,6 +30,7 @@ require (
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.0.0 github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect

View File

@ -173,6 +173,11 @@ func (c *Config) EnsureComplete() error {
return nil return nil
} }
// SetFs allows to set custom filesystem used in Config object. Required for unit tests
func (c *Config) SetFs(fsys fs.FileSystem) {
c.fileSystem = fsys
}
// PersistConfig updates the airshipctl config file to match // PersistConfig updates the airshipctl config file to match
// the current Config object. // the current Config object.
// If file did not previously exist, the file will be created. // If file did not previously exist, the file will be created.
@ -278,24 +283,21 @@ func (c *Config) GetManagementConfiguration(name string) (*ManagementConfigurati
// AddContext creates a new context and returns the instance of // AddContext creates a new context and returns the instance of
// newly created context // newly created context
func (c *Config) AddContext(theContext *ContextOptions) *Context { func (c *Config) AddContext(ctxName string, opts ...ContextOption) *Context {
// Create the new Airship config context // Create the new Airship config context
nContext := NewContext() nContext := NewContext()
c.Contexts[theContext.Name] = nContext c.Contexts[ctxName] = nContext
// Ok , I have initialized structs for the Context information // Ok , I have initialized structs for the Context information
// We can use Modify to populate the correct information // We can use Modify to populate the correct information
c.ModifyContext(nContext, theContext) c.ModifyContext(nContext, opts...)
return nContext return nContext
} }
// ModifyContext updates Context object with given given context options // ModifyContext updates Context object with given context options
func (c *Config) ModifyContext(context *Context, theContext *ContextOptions) { func (c *Config) ModifyContext(context *Context, opts ...ContextOption) {
if theContext.ManagementConfiguration != "" { for _, o := range opts {
context.ManagementConfiguration = theContext.ManagementConfiguration o(context)
}
if theContext.Manifest != "" {
context.Manifest = theContext.Manifest
} }
} }

View File

@ -15,58 +15,72 @@ limitations under the License.
package config package config
import ( import (
"errors" "fmt"
"io"
) )
// ContextOption is a function that allows to modify context object
type ContextOption func(ctx *Context)
// SetContextManifest sets manifest in context object
func SetContextManifest(manifest string) ContextOption {
return func(ctx *Context) {
ctx.Manifest = manifest
}
}
// SetContextManagementConfig sets management config in context object
func SetContextManagementConfig(managementConfig string) ContextOption {
return func(ctx *Context) {
ctx.ManagementConfiguration = managementConfig
}
}
// RunSetContextOptions are options required to create/modify airshipctl context
type RunSetContextOptions struct {
CfgFactory Factory
CtxName string
Current bool
Writer io.Writer
}
// RunSetContext validates the given command line options and invokes AddContext/ModifyContext // RunSetContext validates the given command line options and invokes AddContext/ModifyContext
func RunSetContext(o *ContextOptions, airconfig *Config, writeToStorage bool) (bool, error) { func (o *RunSetContextOptions) RunSetContext(opts ...ContextOption) error {
modified := false cfg, err := o.CfgFactory()
err := o.Validate()
if err != nil { if err != nil {
return modified, err return err
} }
if o.Current { if o.Current {
if airconfig.CurrentContext == "" { o.CtxName = cfg.CurrentContext
return modified, ErrMissingCurrentContext{}
}
// when --current flag is passed, use current context
o.Name = airconfig.CurrentContext
} }
context, err := airconfig.GetContext(o.Name) if o.CtxName == "" {
if err != nil { return ErrEmptyContextName{}
var cerr ErrMissingConfig }
if !errors.As(err, &cerr) {
// An error occurred, but it wasn't a "missing" config error.
return modified, err
}
if o.CurrentContext { infoMsg := fmt.Sprintf("context with name %s", o.CtxName)
return modified, ErrMissingConfig{} context, err := cfg.GetContext(o.CtxName)
} if err != nil {
// context didn't exist, create it // context didn't exist, create it
// ignoring the returned added context cfg.AddContext(o.CtxName, opts...)
airconfig.AddContext(o) infoMsg = fmt.Sprintf("%s created\n", infoMsg)
} else { } else {
// Found the desired Current Context // Context exists, lets update
// Lets update it and be done. cfg.ModifyContext(context, opts...)
if o.CurrentContext { infoMsg = fmt.Sprintf("%s modified\n", infoMsg)
airconfig.CurrentContext = o.Name }
} else {
// Context exists, lets update // Verify we didn't break anything
airconfig.ModifyContext(context, o) if err = cfg.EnsureComplete(); err != nil {
} return err
modified = true }
if _, err := o.Writer.Write([]byte(infoMsg)); err != nil {
return err
} }
// Update configuration file just in time persistence approach // Update configuration file just in time persistence approach
if writeToStorage { return cfg.PersistConfig(true)
if err := airconfig.PersistConfig(true); err != nil {
// Error that it didnt persist the changes
return modified, ErrConfigFailed{}
}
}
return modified, nil
} }
// RunUseContext validates the given context name and updates it as current context // RunUseContext validates the given context name and updates it as current context

View File

@ -15,34 +15,117 @@ limitations under the License.
package config_test package config_test
import ( import (
"bytes"
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kustfs "sigs.k8s.io/kustomize/api/filesys"
"opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/testutil" "opendev.org/airship/airshipctl/testutil"
testfs "opendev.org/airship/airshipctl/testutil/fs"
) )
func prepareConfig() func() (*config.Config, error) {
return func() (*config.Config, error) {
cfg := testutil.DummyConfig()
cfg.SetLoadedConfigPath("test")
cfg.SetFs(testfs.MockFileSystem{
FileSystem: kustfs.MakeFsInMemory(),
MockChmod: func(s string, mode os.FileMode) error {
return nil
},
MockDir: func(s string) string {
return "."
},
})
return cfg, nil
}
}
func TestRunSetContext(t *testing.T) { func TestRunSetContext(t *testing.T) {
t.Run("testAddContext", func(t *testing.T) { ioBuffer := bytes.NewBuffer(nil)
conf := testutil.DummyConfig() tests := []struct {
dummyContextOptions := testutil.DummyContextOptions() name string
dummyContextOptions.Name = "second_context" options config.RunSetContextOptions
ctxopts []config.ContextOption
expectedOut string
err error
}{
{
name: "create new context",
options: config.RunSetContextOptions{
CfgFactory: prepareConfig(),
CtxName: "new_context",
Current: false,
Writer: ioBuffer,
},
ctxopts: []config.ContextOption{config.SetContextManifest("dummy_manifest"),
config.SetContextManagementConfig("dummy_management_config")},
err: nil,
expectedOut: "context with name new_context created\n",
},
{
name: "modify current context",
options: config.RunSetContextOptions{
CfgFactory: prepareConfig(),
CtxName: "",
Current: true,
Writer: ioBuffer,
},
ctxopts: []config.ContextOption{config.SetContextManagementConfig("")},
err: nil,
expectedOut: "context with name dummy_context modified\n",
},
{
name: "bad config",
options: config.RunSetContextOptions{
CfgFactory: func() (*config.Config, error) {
return nil, config.ErrMissingConfig{What: "bad config"}
},
},
err: config.ErrMissingConfig{What: "bad config"},
},
{
name: "no context name provided",
options: config.RunSetContextOptions{
CfgFactory: prepareConfig(),
CtxName: "",
Current: false,
},
err: config.ErrEmptyContextName{},
},
{
name: "setup invalid manifest",
options: config.RunSetContextOptions{
CfgFactory: prepareConfig(),
CtxName: "",
Current: true,
Writer: ioBuffer,
},
ctxopts: []config.ContextOption{config.SetContextManifest("invalid_manifest")},
err: config.ErrMissingConfig{What: "Current Context (dummy_context) does not identify a defined Manifest"},
},
}
modified, err := config.RunSetContext(dummyContextOptions, conf, false) for _, tt := range tests {
assert.NoError(t, err) tt := tt
assert.False(t, modified) t.Run(tt.name, func(t *testing.T) {
assert.Contains(t, conf.Contexts, "second_context") ioBuffer.Reset()
}) err := tt.options.RunSetContext(tt.ctxopts...)
if tt.err != nil {
t.Run("testModifyContext", func(t *testing.T) { require.Error(t, err)
conf := testutil.DummyConfig() require.Equal(t, tt.err, err)
dummyContextOptions := testutil.DummyContextOptions() } else {
require.NoError(t, err)
modified, err := config.RunSetContext(dummyContextOptions, conf, false) }
assert.NoError(t, err) if tt.expectedOut != "" {
assert.True(t, modified) require.Equal(t, tt.expectedOut, ioBuffer.String())
}) }
})
}
} }
func TestRunUseContext(t *testing.T) { func TestRunUseContext(t *testing.T) {

View File

@ -257,7 +257,8 @@ func TestAddContext(t *testing.T) {
defer cleanup(t) defer cleanup(t)
co := testutil.DummyContextOptions() co := testutil.DummyContextOptions()
context := conf.AddContext(co) context := conf.AddContext(co.Name, config.SetContextManifest(co.Manifest),
config.SetContextManagementConfig(co.ManagementConfiguration))
assert.EqualValues(t, conf.Contexts[co.Name], context) assert.EqualValues(t, conf.Contexts[co.Name], context)
} }
@ -266,10 +267,10 @@ func TestModifyContext(t *testing.T) {
defer cleanup(t) defer cleanup(t)
co := testutil.DummyContextOptions() co := testutil.DummyContextOptions()
context := conf.AddContext(co) context := conf.AddContext(co.Name, config.SetContextManifest(co.Manifest))
co.Manifest += stringDelta co.Manifest += stringDelta
conf.ModifyContext(context, co) conf.ModifyContext(context, config.SetContextManifest(co.Manifest))
assert.EqualValues(t, conf.Contexts[co.Name].Manifest, co.Manifest) assert.EqualValues(t, conf.Contexts[co.Name].Manifest, co.Manifest)
assert.EqualValues(t, conf.Contexts[co.Name], context) assert.EqualValues(t, conf.Contexts[co.Name], context)
} }