diff --git a/cmd/config/config.go b/cmd/config/config.go index 4ddc5f8c3..49e9c86b7 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -17,6 +17,8 @@ like "airshipctl config set-current-context my-context" `), } configRootCmd.AddCommand(NewCmdConfigSetCluster(rootSettings)) configRootCmd.AddCommand(NewCmdConfigGetCluster(rootSettings)) + configRootCmd.AddCommand(NewCmdConfigSetContext(rootSettings)) + configRootCmd.AddCommand(NewCmdConfigGetContext(rootSettings)) return configRootCmd } diff --git a/cmd/config/get_cluster.go b/cmd/config/get_cluster.go index b04fd501b..a0c7e0954 100644 --- a/cmd/config/get_cluster.go +++ b/cmd/config/get_cluster.go @@ -27,8 +27,7 @@ import ( ) var ( - getClusterLong = (` -Gets a specific cluster or all defined clusters if no name is provided`) + getClusterLong = (`Display a specific cluster or all defined clusters if no name is provided`) getClusterExample = fmt.Sprintf(` # List all the clusters airshipctl knows about @@ -44,8 +43,7 @@ func NewCmdConfigGetCluster(rootSettings *environment.AirshipCTLSettings) *cobra theCluster := &config.ClusterOptions{} getclustercmd := &cobra.Command{ Use: "get-cluster NAME", - Short: "Display a specific cluster", - Long: getClusterLong, + Short: getClusterLong, Example: getClusterExample, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { diff --git a/cmd/config/get_context.go b/cmd/config/get_context.go new file mode 100644 index 000000000..b77048457 --- /dev/null +++ b/cmd/config/get_context.go @@ -0,0 +1,107 @@ +/* +Copyright 2014 The Kubernetes 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 config + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/environment" +) + +var ( + getContextLong = (`Display a specific context, the current-context or all defined contexts if no name is provided`) + + getContextExample = fmt.Sprintf(`# List all the contexts airshipctl knows about +airshipctl config get-context + +# Display the current context +airshipctl config get-context --%v + +# Display a specific Context +airshipctl config get-context e2e`, + config.FlagCurrentContext) +) + +// A Context refers to a particular cluster, however it does not specify which of the cluster types +// it relates to. Getting explicit information about a particular context will depend +// on the ClusterType flag. + +// NewCmdConfigGetContext returns a Command instance for 'config -Context' sub command +func NewCmdConfigGetContext(rootSettings *environment.AirshipCTLSettings) *cobra.Command { + + theContext := &config.ContextOptions{} + getcontextcmd := &cobra.Command{ + Use: "get-context NAME", + Short: getContextLong, + Example: getContextExample, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + theContext.Name = args[0] + } + return runGetContext(theContext, cmd.OutOrStdout(), rootSettings.Config()) + }, + } + + gctxInitFlags(theContext, getcontextcmd) + + return getcontextcmd +} + +func gctxInitFlags(o *config.ContextOptions, getcontextcmd *cobra.Command) { + getcontextcmd.Flags().BoolVar(&o.CurrentContext, config.FlagCurrentContext, false, + config.FlagCurrentContext+" to retrieve the current context entry in airshipctl config") + +} + +// runGetContext performs the execution of 'config get-Context' sub command +func runGetContext(o *config.ContextOptions, out io.Writer, airconfig *config.Config) error { + if o.Name == "" && !o.CurrentContext { + return getContexts(out, airconfig) + } + return getContext(o, out, airconfig) +} + +func getContext(o *config.ContextOptions, out io.Writer, airconfig *config.Config) error { + cName := o.Name + if o.CurrentContext { + cName = airconfig.CurrentContext + } + context, err := airconfig.GetContext(cName) + if err != nil { + return err + } + fmt.Fprintf(out, "%s", context.PrettyString()) + return nil +} + +func getContexts(out io.Writer, airconfig *config.Config) error { + contexts, err := airconfig.GetContexts() + if err != nil { + return err + } + if contexts == nil { + fmt.Fprint(out, "No Contexts found in the configuration.\n") + } + for _, context := range contexts { + fmt.Fprintf(out, "%s", context.PrettyString()) + } + return nil +} diff --git a/cmd/config/get_context_test.go b/cmd/config/get_context_test.go new file mode 100644 index 000000000..9f85ec801 --- /dev/null +++ b/cmd/config/get_context_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2014 The Kubernetes 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 config_test + +import ( + "fmt" + "testing" + + kubeconfig "k8s.io/client-go/tools/clientcmd/api" + + cmd "opendev.org/airship/airshipctl/cmd/config" + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/environment" + "opendev.org/airship/airshipctl/testutil" +) + +const ( + currentContextFlag = "--" + config.FlagCurrentContext + + fooContext = "ContextFoo" + barContext = "ContextBar" + bazContext = "ContextBaz" + missingContext = "contextMissing" +) + +func TestGetContextCmd(t *testing.T) { + conf := &config.Config{ + Contexts: map[string]*config.Context{ + fooContext: getNamedTestContext(fooContext), + barContext: getNamedTestContext(barContext), + bazContext: getNamedTestContext(bazContext), + }, + CurrentContext: bazContext, + } + + settings := &environment.AirshipCTLSettings{} + settings.SetConfig(conf) + + cmdTests := []*testutil.CmdTest{ + { + Name: "get-context", + CmdLine: fmt.Sprintf("%s", fooContext), + Cmd: cmd.NewCmdConfigGetContext(settings), + }, + { + Name: "get-all-contexts", + CmdLine: fmt.Sprintf("%s %s", fooContext, barContext), + Cmd: cmd.NewCmdConfigGetContext(settings), + }, + // This is not implemented yet + { + Name: "get-multiple-contexts", + CmdLine: fmt.Sprintf("%s %s", fooContext, barContext), + Cmd: cmd.NewCmdConfigGetContext(settings), + }, + + { + Name: "missing", + CmdLine: fmt.Sprintf("%s", missingContext), + Cmd: cmd.NewCmdConfigGetContext(settings), + Error: fmt.Errorf("Context %s information was not "+ + "found in the configuration.", missingContext), + }, + { + Name: "get-current-context", + CmdLine: fmt.Sprintf("%s", currentContextFlag), + Cmd: cmd.NewCmdConfigGetContext(settings), + }, + } + + for _, tt := range cmdTests { + testutil.RunTest(t, tt) + } +} + +func TestNoContextsGetContextCmd(t *testing.T) { + settings := &environment.AirshipCTLSettings{} + settings.SetConfig(&config.Config{}) + cmdTest := &testutil.CmdTest{ + Name: "no-contexts", + CmdLine: "", + Cmd: cmd.NewCmdConfigGetContext(settings), + } + testutil.RunTest(t, cmdTest) +} + +func getNamedTestContext(contextName string) *config.Context { + + kContext := &kubeconfig.Context{ + Namespace: "dummy_namespace", + AuthInfo: "dummy_user", + Cluster: fmt.Sprintf("dummycluster_%s", config.Ephemeral), + } + + newContext := &config.Context{ + NameInKubeconf: fmt.Sprintf("%s_%s", contextName, config.Ephemeral), + Manifest: fmt.Sprintf("Manifest_%s", contextName), + } + newContext.SetKubeContext(kContext) + + return newContext +} diff --git a/cmd/config/set_context.go b/cmd/config/set_context.go new file mode 100644 index 000000000..abaa157dc --- /dev/null +++ b/cmd/config/set_context.go @@ -0,0 +1,141 @@ +/* +Copyright 2016 The Kubernetes 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 config + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/environment" + conferrors "opendev.org/airship/airshipctl/pkg/errors" +) + +var ( + setContextLong = (` +Sets a context entry in arshipctl config. +Specifying a name that already exists will merge new fields on top of existing values for those fields.`) + + setContextExample = fmt.Sprintf(` +# Create a completely new e2e context entry +airshipctl config set-context e2e --%v=kube-system --%v=manifest --%v=auth-info --%v=%v + +# Update the current-context to e2e +airshipctl config set-context e2e --%v=true`, + config.FlagNamespace, + config.FlagManifest, + config.FlagAuthInfoName, + config.FlagClusterType, + config.Target, + config.FlagCurrentContext) +) + +// NewCmdConfigSetContext creates a command object for the "set-context" action, which +// defines a new Context airship config. +func NewCmdConfigSetContext(rootSettings *environment.AirshipCTLSettings) *cobra.Command { + theContext := &config.ContextOptions{} + + setcontextcmd := &cobra.Command{ + Use: "set-context NAME", + Short: "Sets a context entry or updates current-context in the airshipctl config", + Long: setContextLong, + Example: setContextExample, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + theContext.Name = cmd.Flags().Args()[0] + modified, err := runSetContext(theContext, rootSettings.Config()) + if err != nil { + return err + } + if modified { + fmt.Fprintf(cmd.OutOrStdout(), "Context %q modified.\n", theContext.Name) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Context %q created.\n", theContext.Name) + } + return nil + }, + } + + sctxInitFlags(theContext, setcontextcmd) + return setcontextcmd +} + +func sctxInitFlags(o *config.ContextOptions, setcontextcmd *cobra.Command) { + + setcontextcmd.Flags().BoolVar(&o.CurrentContext, config.FlagCurrentContext, false, + config.FlagCurrentContext+" for the context entry in airshipctl config") + + setcontextcmd.Flags().StringVar(&o.Cluster, config.FlagClusterName, o.Cluster, + config.FlagClusterName+" for the context entry in airshipctl config") + + setcontextcmd.Flags().StringVar(&o.AuthInfo, config.FlagAuthInfoName, o.AuthInfo, + config.FlagAuthInfoName+" for the context entry in airshipctl config") + + setcontextcmd.Flags().StringVar(&o.Manifest, config.FlagManifest, o.Manifest, + config.FlagManifest+" for the context entry in airshipctl config") + + setcontextcmd.Flags().StringVar(&o.Namespace, config.FlagNamespace, o.Namespace, + config.FlagNamespace+" for the context entry in airshipctl config") + + setcontextcmd.Flags().StringVar(&o.ClusterType, config.FlagClusterType, "", + config.FlagClusterType+" for the context entry in airshipctl config") + +} + +func runSetContext(o *config.ContextOptions, airconfig *config.Config) (bool, error) { + contextWasModified := false + err := o.Validate() + if err != nil { + return contextWasModified, err + } + + contextIWant := o.Name + context, err := airconfig.GetContext(contextIWant) + if err != nil { + var cerr conferrors.ErrMissingConfig + if !errors.As(err, &cerr) { + // An error occurred, but it wasn't a "missing" config error. + return contextWasModified, err + } + + if o.CurrentContext { + return contextWasModified, conferrors.ErrMissingConfig{} + } + // context didn't exist, create it + // ignoring the returned added context + airconfig.AddContext(o) + } else { + // Found the desired Current Context + // Lets update it and be done. + if o.CurrentContext { + airconfig.CurrentContext = o.Name + } else { + // Context exists, lets update + airconfig.ModifyContext(context, o) + } + contextWasModified = true + } + // Update configuration file just in time persistence approach + if err := airconfig.PersistConfig(); err != nil { + // Error that it didnt persist the changes + return contextWasModified, conferrors.ErrConfigFailed{} + } + + return contextWasModified, nil +} diff --git a/cmd/config/set_context_test.go b/cmd/config/set_context_test.go new file mode 100644 index 000000000..38bc13a8b --- /dev/null +++ b/cmd/config/set_context_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2017 The Kubernetes 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 config + +import ( + "bytes" + "testing" + + kubeconfig "k8s.io/client-go/tools/clientcmd/api" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/environment" + "opendev.org/airship/airshipctl/testutil" +) + +const ( + testUser = "admin@kubernetes" +) + +type setContextTest struct { + description string + config *config.Config + args []string + flags []string + expected string + expectedConfig *config.Config +} + +func TestConfigSetContext(t *testing.T) { + + cmdTests := []*testutil.CmdTest{ + { + Name: "config-cmd-set-context-with-help", + CmdLine: "--help", + Cmd: NewCmdConfigSetContext(nil), + }, + } + + for _, tt := range cmdTests { + testutil.RunTest(t, tt) + } +} + +func TestSetContext(t *testing.T) { + + conf := config.InitConfig(t) + + tname := "dummycontext" + tctype := config.Ephemeral + + expconf := config.InitConfig(t) + expconf.Contexts[tname] = config.NewContext() + clusterName := config.NewClusterComplexName() + clusterName.WithType(tname, tctype) + expconf.Contexts[tname].NameInKubeconf = clusterName.Name() + expconf.Contexts[tname].Manifest = "edge_cloud" + + expkContext := kubeconfig.NewContext() + expkContext.AuthInfo = testUser + expkContext.Namespace = "kube-system" + expconf.KubeConfig().Contexts[expconf.Contexts[tname].NameInKubeconf] = expkContext + + test := setContextTest{ + description: "Testing 'airshipctl config set-context' with a new context", + config: conf, + args: []string{tname}, + flags: []string{ + "--" + config.FlagClusterType + "=" + config.Target, + "--" + config.FlagAuthInfoName + "=" + testUser, + "--" + config.FlagManifest + "=edge_cloud", + "--" + config.FlagNamespace + "=kube-system", + }, + expected: `Context "` + tname + `" created.` + "\n", + expectedConfig: expconf, + } + test.run(t) +} + +func TestSetCurrentContext(t *testing.T) { + tname := "def_target" + conf := config.InitConfig(t) + + expconf := config.InitConfig(t) + expconf.CurrentContext = "def_target" + + test := setContextTest{ + description: "Testing 'airshipctl config set-context' with a new current context", + config: conf, + args: []string{tname}, + flags: []string{ + "--" + config.FlagCurrentContext + "=true", + }, + expected: `Context "` + tname + `" modified.` + "\n", + expectedConfig: expconf, + } + test.run(t) +} +func TestModifyContext(t *testing.T) { + tname := testCluster + tctype := config.Ephemeral + + conf := config.InitConfig(t) + conf.Contexts[tname] = config.NewContext() + + clusterName := config.NewClusterComplexName() + clusterName.WithType(tname, tctype) + conf.Contexts[tname].NameInKubeconf = clusterName.Name() + kContext := kubeconfig.NewContext() + kContext.AuthInfo = testUser + conf.KubeConfig().Contexts[clusterName.Name()] = kContext + conf.Contexts[tname].SetKubeContext(kContext) + + expconf := config.InitConfig(t) + expconf.Contexts[tname] = config.NewContext() + expconf.Contexts[tname].NameInKubeconf = clusterName.Name() + expkContext := kubeconfig.NewContext() + expkContext.AuthInfo = testUser + expconf.KubeConfig().Contexts[clusterName.Name()] = expkContext + expconf.Contexts[tname].SetKubeContext(expkContext) + + test := setContextTest{ + description: "Testing 'airshipctl config set-context' with an existing context", + config: conf, + args: []string{tname}, + flags: []string{ + "--" + config.FlagAuthInfoName + "=" + testUser, + }, + expected: `Context "` + tname + `" modified.` + "\n", + expectedConfig: expconf, + } + test.run(t) +} + +func (test setContextTest) run(t *testing.T) { + + // Get the Environment + settings := &environment.AirshipCTLSettings{} + settings.SetConfig(test.config) + + buf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdConfigSetContext(settings) + cmd.SetOutput(buf) + cmd.SetArgs(test.args) + err := cmd.Flags().Parse(test.flags) + require.NoErrorf(t, err, "unexpected error flags args to command: %v, flags: %v", err, test.flags) + + // Execute the Command + // Which should Persist the File + err = cmd.Execute() + require.NoErrorf(t, err, "unexpected error executing command: %v, args: %v, flags: %v", err, test.args, test.flags) + + afterRunConf := settings.Config() + + // Find the Context Created or Modified + afterRunContext, err := afterRunConf.GetContext(test.args[0]) + require.NoError(t, err) + require.NotNil(t, afterRunContext) + + afterKcontext := afterRunContext.KubeContext() + require.NotNil(t, afterKcontext) + + testKcontext := test.expectedConfig.KubeConfig().Contexts[test.expectedConfig.Contexts[test.args[0]].NameInKubeconf] + require.NotNil(t, testKcontext) + + assert.EqualValues(t, afterKcontext.AuthInfo, testKcontext.AuthInfo) + + // Test that the Return Message looks correct + if len(test.expected) != 0 { + assert.EqualValuesf(t, buf.String(), test.expected, "expected %v, but got %v", test.expected, buf.String()) + } + +} diff --git a/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden b/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden index 79a8d07b7..0b45f824e 100644 --- a/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden +++ b/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden @@ -5,9 +5,11 @@ Usage: config [command] Available Commands: - get-cluster Display a specific cluster + get-cluster Display a specific cluster or all defined clusters if no name is provided + get-context Display a specific context, the current-context or all defined contexts if no name is provided help Help about any command set-cluster Sets a cluster entry in the airshipctl config + set-context Sets a context entry or updates current-context in the airshipctl config Flags: -h, --help help for config diff --git a/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-help.golden b/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-help.golden index 79a8d07b7..0b45f824e 100644 --- a/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-help.golden +++ b/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-help.golden @@ -5,9 +5,11 @@ Usage: config [command] Available Commands: - get-cluster Display a specific cluster + get-cluster Display a specific cluster or all defined clusters if no name is provided + get-context Display a specific context, the current-context or all defined contexts if no name is provided help Help about any command set-cluster Sets a cluster entry in the airshipctl config + set-context Sets a context entry or updates current-context in the airshipctl config Flags: -h, --help help for config diff --git a/cmd/config/testdata/TestConfigSetContextGoldenOutput/config-cmd-set-context-with-help.golden b/cmd/config/testdata/TestConfigSetContextGoldenOutput/config-cmd-set-context-with-help.golden new file mode 100644 index 000000000..9d373be24 --- /dev/null +++ b/cmd/config/testdata/TestConfigSetContextGoldenOutput/config-cmd-set-context-with-help.golden @@ -0,0 +1,23 @@ + +Sets a context entry in arshipctl config. +Specifying a name that already exists will merge new fields on top of existing values for those fields. + +Usage: + set-context NAME [flags] + +Examples: + +# Create a completely new e2e context entry +airshipctl config set-context e2e --namespace=kube-system --manifest=manifest --user=auth-info --cluster-type=target + +# Update the current-context to e2e +airshipctl config set-context e2e --current-context=true + +Flags: + --cluster string cluster for the context entry in airshipctl config + --cluster-type string cluster-type for the context entry in airshipctl config + --current-context current-context for the context entry in airshipctl config + -h, --help help for set-context + --manifest string manifest for the context entry in airshipctl config + --namespace string namespace for the context entry in airshipctl config + --user string user for the context entry in airshipctl config diff --git a/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-all-contexts.golden b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-all-contexts.golden new file mode 100644 index 000000000..946c34334 --- /dev/null +++ b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-all-contexts.golden @@ -0,0 +1,27 @@ +Context: ContextBar +context-kubeconf: ContextBar_ephemeral +manifest: Manifest_ContextBar + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + +Context: ContextBaz +context-kubeconf: ContextBaz_ephemeral +manifest: Manifest_ContextBaz + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + +Context: ContextFoo +context-kubeconf: ContextFoo_ephemeral +manifest: Manifest_ContextFoo + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + diff --git a/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-context.golden b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-context.golden new file mode 100644 index 000000000..3aecd1940 --- /dev/null +++ b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-context.golden @@ -0,0 +1,9 @@ +Context: ContextFoo +context-kubeconf: ContextFoo_ephemeral +manifest: Manifest_ContextFoo + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + diff --git a/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-current-context.golden b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-current-context.golden new file mode 100644 index 000000000..bbcedb6bd --- /dev/null +++ b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-current-context.golden @@ -0,0 +1,9 @@ +Context: ContextBaz +context-kubeconf: ContextBaz_ephemeral +manifest: Manifest_ContextBaz + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + diff --git a/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-multiple-contexts.golden b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-multiple-contexts.golden new file mode 100644 index 000000000..946c34334 --- /dev/null +++ b/cmd/config/testdata/TestGetContextCmdGoldenOutput/get-multiple-contexts.golden @@ -0,0 +1,27 @@ +Context: ContextBar +context-kubeconf: ContextBar_ephemeral +manifest: Manifest_ContextBar + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + +Context: ContextBaz +context-kubeconf: ContextBaz_ephemeral +manifest: Manifest_ContextBaz + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + +Context: ContextFoo +context-kubeconf: ContextFoo_ephemeral +manifest: Manifest_ContextFoo + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user + diff --git a/cmd/config/testdata/TestGetContextCmdGoldenOutput/missing.golden b/cmd/config/testdata/TestGetContextCmdGoldenOutput/missing.golden new file mode 100644 index 000000000..8862d63a0 --- /dev/null +++ b/cmd/config/testdata/TestGetContextCmdGoldenOutput/missing.golden @@ -0,0 +1,18 @@ +Error: Missing configuration +Usage: + get-context NAME [flags] + +Examples: +# List all the contexts airshipctl knows about +airshipctl config get-context + +# Display the current context +airshipctl config get-context --current-context + +# Display a specific Context +airshipctl config get-context e2e + +Flags: + --current-context current-context to retrieve the current context entry in airshipctl config + -h, --help help for get-context + diff --git a/cmd/config/testdata/TestNoContextsGetContextCmdGoldenOutput/no-contexts.golden b/cmd/config/testdata/TestNoContextsGetContextCmdGoldenOutput/no-contexts.golden new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/config/cmds.go b/pkg/config/cmds.go index 1b4901eac..5cf769393 100644 --- a/pkg/config/cmds.go +++ b/pkg/config/cmds.go @@ -47,3 +47,18 @@ func (o *ClusterOptions) Validate() error { } return nil } + +func (o *ContextOptions) Validate() error { + if len(o.Name) == 0 { + return errors.New("you must specify a non-empty context name") + } + // Expect ClusterType only when this is not setting currentContext + if o.ClusterType != "" { + err := ValidClusterType(o.ClusterType) + if err != nil { + return err + } + } + // TODO Manifest, Cluster could be validated against the existing config maps + return nil +} diff --git a/pkg/config/cmds_test.go b/pkg/config/cmds_test.go index 4d0ec91e8..cae23e8b8 100644 --- a/pkg/config/cmds_test.go +++ b/pkg/config/cmds_test.go @@ -22,7 +22,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestValidate(t *testing.T) { +func TestValidateCluster(t *testing.T) { co := DummyClusterOptions() // Assert that the initial dummy config is valid @@ -61,3 +61,11 @@ func TestValidate(t *testing.T) { err = co.Validate() assert.Error(t, err) } + +func TestValidateContext(t *testing.T) { + co := DummyContextOptions() + // Valid Data case + err := co.Validate() + assert.NoError(t, err) + +} diff --git a/pkg/config/cmds_types.go b/pkg/config/cmds_types.go index d862d4f5a..a6190ebc9 100644 --- a/pkg/config/cmds_types.go +++ b/pkg/config/cmds_types.go @@ -8,3 +8,13 @@ type ClusterOptions struct { CertificateAuthority string EmbedCAData bool } + +type ContextOptions struct { + Name string + ClusterType string + CurrentContext bool + Cluster string + AuthInfo string + Manifest string + Namespace string +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 5da620893..74288ec82 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -32,6 +32,7 @@ import ( kubeconfig "k8s.io/client-go/tools/clientcmd/api" + conferrors "opendev.org/airship/airshipctl/pkg/errors" "opendev.org/airship/airshipctl/pkg/util" ) @@ -97,7 +98,7 @@ func (c *Config) reconcileConfig() error { // I changed things during the reconciliation // Lets reflect them in the config files - // Specially useful if the cnofig is loaded during a get operation + // Specially useful if the config is loaded during a get operation // If it was a Set this would have happened eventually any way if persistIt { return c.PersistConfig() @@ -211,6 +212,7 @@ func (c *Config) reconcileContexts(updatedClusterNames map[string]string) { } // Make sure the name matches c.Contexts[key].NameInKubeconf = context.Cluster + c.Contexts[key].SetKubeContext(context) // What about if a Context refers to a properly named cluster // that does not exist in airship config @@ -267,8 +269,6 @@ func (c *Config) reconcileCurrentContext() { c.kubeConfig.CurrentContext = c.CurrentContext } } - c.kubeConfig.CurrentContext = "" - c.CurrentContext = "" } // This is called by users of the config to make sure that they have @@ -370,7 +370,6 @@ func (c *Config) KubeConfig() *kubeconfig.Config { return c.kubeConfig } -// This might be changed later to be generalized func (c *Config) ClusterNames() []string { names := []string{} for k := range c.Clusters { @@ -378,7 +377,15 @@ func (c *Config) ClusterNames() []string { } sort.Strings(names) return names +} +func (c *Config) ContextNames() []string { + names := []string{} + for k := range c.Contexts { + names = append(names, k) + } + sort.Strings(names) + return names } // Get A Cluster @@ -476,12 +483,117 @@ func (c *Config) GetClusters() ([]*Cluster, error) { if err == nil { clusters = append(clusters, cluster) } - } } return clusters, nil } +// Context Operations from Config point of view +// Get Context +func (c *Config) GetContext(cName string) (*Context, error) { + context, exists := c.Contexts[cName] + if !exists { + return nil, conferrors.ErrMissingConfig{} + } + return context, nil +} + +func (c *Config) GetContexts() ([]*Context, error) { + contexts := []*Context{} + // Given that we change the testing metholdogy + // The ordered names are no longer required + for _, cName := range c.ContextNames() { + context, err := c.GetContext(cName) + if err == nil { + contexts = append(contexts, context) + } + } + return contexts, nil +} + +func (c *Config) AddContext(theContext *ContextOptions) *Context { + // Create the new Airship config context + nContext := NewContext() + c.Contexts[theContext.Name] = nContext + // Create a new Kubeconfig Context object as well + kContext := kubeconfig.NewContext() + nContext.NameInKubeconf = theContext.Name + contextName := NewClusterComplexName() + contextName.WithType(theContext.Name, theContext.ClusterType) + + nContext.SetKubeContext(kContext) + c.KubeConfig().Contexts[theContext.Name] = kContext + + // Ok , I have initialized structs for the Context information + // We can use Modify to populate the correct information + c.ModifyContext(nContext, theContext) + return nContext + +} + +func (c *Config) ModifyContext(context *Context, theContext *ContextOptions) { + kContext := context.KubeContext() + if kContext == nil { + return + } + if theContext.Cluster != "" { + kContext.Cluster = theContext.Cluster + } + if theContext.AuthInfo != "" { + kContext.AuthInfo = theContext.AuthInfo + } + if theContext.Manifest != "" { + context.Manifest = theContext.Manifest + } + if theContext.Namespace != "" { + kContext.Namespace = theContext.Namespace + } +} + +// CurrentContext methods Returns the appropriate information for the current context +// Current Context holds labels for the approriate config objects +// Cluster is the name of the cluster for this context +// ClusterType is the name of the clustertype for this context, it should be a flag we pass to it?? +// AuthInfo is the name of the authInfo for this context +// Manifest is the default manifest to be use with this context +// Purpose for this method is simplifying the current context information +func (c *Config) GetCurrentContext() (*Context, error) { + if err := c.EnsureComplete(); err != nil { + return nil, err + } + currentContext, err := c.GetContext(c.CurrentContext) + if err != nil { + // this should not happen since Ensure Complete checks for this + return nil, err + } + return currentContext, nil +} +func (c *Config) CurrentContextCluster() (*Cluster, error) { + currentContext, err := c.GetCurrentContext() + if err != nil { + return nil, err + } + + return c.Clusters[currentContext.KubeContext().Cluster].ClusterTypes[currentContext.ClusterType()], nil +} + +func (c *Config) CurrentContextAuthInfo() (*AuthInfo, error) { + currentContext, err := c.GetCurrentContext() + if err != nil { + return nil, err + } + + return c.AuthInfos[currentContext.KubeContext().AuthInfo], nil +} +func (c *Config) CurrentContextManifest() (*Manifest, error) { + currentContext, err := c.GetCurrentContext() + if err != nil { + return nil, err + } + + return c.Manifests[currentContext.Manifest], nil +} + // Purge removes the config file func (c *Config) Purge() error { //configFile := c.ConfigFile() @@ -550,15 +662,44 @@ func (c *Context) Equal(d *Context) bool { return d == c } return c.NameInKubeconf == d.NameInKubeconf && - c.Manifest == d.Manifest + c.Manifest == d.Manifest && + c.kContext == d.kContext } func (c *Context) String() string { - yaml, err := yaml.Marshal(&c) + cyaml, err := yaml.Marshal(&c) if err != nil { return "" } - return string(yaml) + kcluster := c.KubeContext() + kyaml, err := yaml.Marshal(&kcluster) + if err != nil { + return string(cyaml) + } + return fmt.Sprintf("%s\n%s", string(cyaml), string(kyaml)) +} + +func (c *Context) PrettyString() string { + clusterName := NewClusterComplexName() + clusterName.FromName(c.NameInKubeconf) + + return fmt.Sprintf("Context: %s\n%s\n", + clusterName.ClusterName(), c.String()) +} + +func (c *Context) KubeContext() *kubeconfig.Context { + return c.kContext +} + +func (c *Context) SetKubeContext(kc *kubeconfig.Context) { + c.kContext = kc +} + +func (c *Context) ClusterType() string { + clusterName := NewClusterComplexName() + clusterName.FromName(c.NameInKubeconf) + return clusterName.ClusterType() + } // AuthInfo functions @@ -695,3 +836,11 @@ func KClusterString(kCluster *kubeconfig.Cluster) string { return string(yaml) } +func KContextString(kContext *kubeconfig.Context) string { + yaml, err := yaml.Marshal(&kContext) + if err != nil { + return "" + } + + return string(yaml) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index df76596e3..1a33bc45e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -232,11 +232,6 @@ func TestPurge(t *testing.T) { assert.Falsef(t, os.IsExist(err), "Purge failed to remove file at %v", config.LoadedConfigPath()) } -func TestClusterNames(t *testing.T) { - conf := InitConfig(t) - expected := []string{"def", "onlyinkubeconf", "wrongonlyinconfig", "wrongonlyinkubeconf"} - assert.EqualValues(t, expected, conf.ClusterNames()) -} func TestKClusterString(t *testing.T) { conf := InitConfig(t) kClusters := conf.KubeConfig().Clusters @@ -245,6 +240,14 @@ func TestKClusterString(t *testing.T) { } assert.EqualValues(t, KClusterString(nil), "null\n") } +func TestKContextString(t *testing.T) { + conf := InitConfig(t) + kContexts := conf.KubeConfig().Contexts + for kCtx := range kContexts { + assert.NotEmpty(t, KContextString(kContexts[kCtx])) + } + assert.EqualValues(t, KClusterString(nil), "null\n") +} func TestComplexName(t *testing.T) { cName := "aCluster" ctName := Ephemeral @@ -372,3 +375,25 @@ func TestReconcileClusters(t *testing.T) { // Check that the "stragglers" were removed from the airshipconfig assert.NotContains(t, testConfig.Clusters, "straggler") } + +func TestGetContexts(t *testing.T) { + conf := InitConfig(t) + contexts, err := conf.GetContexts() + require.NoError(t, err) + assert.Len(t, contexts, 3) +} + +func TestGetContext(t *testing.T) { + conf := InitConfig(t) + context, err := conf.GetContext("def_ephemeral") + require.NoError(t, err) + + // Test Positives + assert.EqualValues(t, context.NameInKubeconf, "def_ephemeral") + assert.EqualValues(t, context.KubeContext().Cluster, "def_ephemeral") + + // Test Wrong Cluster + _, err = conf.GetContext("unknown") + assert.Error(t, err) + +} diff --git a/pkg/config/constants.go b/pkg/config/constants.go index 00a2baadc..d06c73e8f 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -28,23 +28,24 @@ const ( // Constants defining CLI flags const ( + FlagAPIServer = "server" + FlagAuthInfoName = "user" + FlagBearerToken = "token" + FlagCAFile = "certificate-authority" + FlagCertFile = "client-certificate" FlagClusterName = "cluster" FlagClusterType = "cluster-type" - FlagAuthInfoName = "user" FlagContext = "context" + FlagCurrentContext = "current-context" FlagConfigFilePath = AirshipConfigEnv - FlagNamespace = "namespace" - FlagAPIServer = "server" - FlagInsecure = "insecure-skip-tls-verify" - FlagCertFile = "client-certificate" - FlagKeyFile = "client-key" - FlagCAFile = "certificate-authority" FlagEmbedCerts = "embed-certs" - FlagBearerToken = "token" FlagImpersonate = "as" FlagImpersonateGroup = "as-group" - FlagUsername = "username" + FlagInsecure = "insecure-skip-tls-verify" + FlagKeyFile = "client-key" + FlagManifest = "manifest" + FlagNamespace = "namespace" FlagPassword = "password" FlagTimeout = "request-timeout" - FlagManifest = "manifest" + FlagUsername = "username" ) diff --git a/pkg/config/test_utils.go b/pkg/config/test_utils.go index aa2eaa9f0..86773c1d7 100644 --- a/pkg/config/test_utils.go +++ b/pkg/config/test_utils.go @@ -60,6 +60,12 @@ func DummyContext() *Context { c := NewContext() c.NameInKubeconf = "dummy_cluster" c.Manifest = "dummy_manifest" + context := kubeconfig.NewContext() + context.Namespace = "dummy_namespace" + context.AuthInfo = "dummy_user" + context.Cluster = "dummycluster_ephemeral" + c.SetKubeContext(context) + return c } @@ -144,6 +150,17 @@ func DummyClusterOptions() *ClusterOptions { return co } +func DummyContextOptions() *ContextOptions { + co := &ContextOptions{} + co.Name = "dummy_context" + co.Manifest = "dummy_manifest" + co.AuthInfo = "dummy_user" + co.CurrentContext = false + co.Namespace = "dummy_namespace" + + return co +} + const ( testConfigYAML = `apiVersion: airshipit.org/v1alpha1 clusters: diff --git a/pkg/config/testdata/context-string.yaml b/pkg/config/testdata/context-string.yaml index c1051fe26..b4cca1a5b 100644 --- a/pkg/config/testdata/context-string.yaml +++ b/pkg/config/testdata/context-string.yaml @@ -1,2 +1,7 @@ context-kubeconf: dummy_cluster manifest: dummy_manifest + +LocationOfOrigin: "" +cluster: dummycluster_ephemeral +namespace: dummy_namespace +user: dummy_user diff --git a/pkg/config/types.go b/pkg/config/types.go index 7ab717f2e..174115d2a 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -97,15 +97,18 @@ type Modules struct { Dummy string `json:"dummy-for-tests"` } -// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), +// Context is a tuple of references to a cluster (how do I communicate with a kubernetes context), // a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) type Context struct { - // Context name in kubeconf. Should include a clustername following the naming conventions for airshipctl - // _ + // Context name in kubeconf NameInKubeconf string `json:"context-kubeconf"` + // Manifest is the default manifest to be use with this context // +optional Manifest string `json:"manifest,omitempty"` + + // Kubeconfig Context Object + kContext *kubeconfig.Context } type AuthInfo struct { diff --git a/pkg/errors/common.go b/pkg/errors/common.go index 16ec47ce1..80353eaab 100644 --- a/pkg/errors/common.go +++ b/pkg/errors/common.go @@ -1,11 +1,24 @@ package errors +// AirshipError is the base error type +// used to create extended error types +// in other airshipctl packages. +type AirshipError struct { + Message string +} + +// Error function implments the golang +// error interface +func (ae *AirshipError) Error() string { + return ae.Message +} + // ErrNotImplemented returned for not implemented features type ErrNotImplemented struct { } func (e ErrNotImplemented) Error() string { - return "Error. Not implemented" + return "Not implemented" } // ErrWrongConfig returned in case of incorrect configuration @@ -13,5 +26,21 @@ type ErrWrongConfig struct { } func (e ErrWrongConfig) Error() string { - return "Error. Wrong configuration" + return "Wrong configuration" +} + +// ErrMissingConfig returned in case of missing configuration +type ErrMissingConfig struct { +} + +func (e ErrMissingConfig) Error() string { + return "Missing configuration" +} + +// ErrConfigFailed returned in case of failure during configuration +type ErrConfigFailed struct { +} + +func (e ErrConfigFailed) Error() string { + return "Configuration failed to complete." }