From 36a302fce14e6cef77614620948843dedf9a4426 Mon Sep 17 00:00:00 2001 From: Rodolfo Pacheco Date: Tue, 4 Feb 2020 13:00:21 -0500 Subject: [PATCH] Introduces config cmd's for set-credentials and get-credentials to manage authentication information for clusters. Includes username/password, certificate and token options. Change-Id: If95e5bbf5c3ddc4732465e81de407d5ad416e8f2 --- cmd/config/config.go | 4 +- cmd/config/get_authinfo.go | 89 ++++++++++ cmd/config/get_authinfo_test.go | 106 ++++++++++++ cmd/config/set_authinfo.go | 162 ++++++++++++++++++ cmd/config/set_authinfo_test.go | 158 +++++++++++++++++ cmd/config/set_cluster.go | 2 +- cmd/config/set_context.go | 2 +- .../config-cmd-with-defaults.golden | 16 +- .../config-cmd-with-help.golden | 16 +- .../config-cmd-set-authinfo-with-help.golden | 37 ++++ .../get-all-credentials.golden | 21 +++ .../get-credentials.golden | 7 + .../get-multiple-credentials.golden | 21 +++ .../missing.golden | 14 ++ .../no-credentials.golden | 1 + pkg/config/cmds.go | 25 +++ pkg/config/cmds_test.go | 12 ++ pkg/config/cmds_types.go | 10 ++ pkg/config/config.go | 80 ++++++++- pkg/config/config_test.go | 89 +++++++++- pkg/config/test_utils.go | 28 ++- pkg/config/testdata/authinfo-string.yaml | 7 +- pkg/config/types.go | 4 +- 23 files changed, 882 insertions(+), 29 deletions(-) create mode 100644 cmd/config/get_authinfo.go create mode 100644 cmd/config/get_authinfo_test.go create mode 100644 cmd/config/set_authinfo.go create mode 100644 cmd/config/set_authinfo_test.go create mode 100644 cmd/config/testdata/TestConfigSetAuthInfoGoldenOutput/config-cmd-set-authinfo-with-help.golden create mode 100644 cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-all-credentials.golden create mode 100644 cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-credentials.golden create mode 100644 cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-multiple-credentials.golden create mode 100644 cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/missing.golden create mode 100644 cmd/config/testdata/TestNoAuthInfosGetAuthInfoCmdGoldenOutput/no-credentials.golden diff --git a/cmd/config/config.go b/cmd/config/config.go index 31d1cd2b2..b7d136692 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -13,13 +13,15 @@ func NewConfigCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Comma DisableFlagsInUseLine: true, Short: ("Modify airshipctl config files"), Long: (`Modify airshipctl config files using subcommands -like "airshipctl config set-current-context my-context" `), +like "airshipctl config set-context --current-context my-context" `), } configRootCmd.AddCommand(NewCmdConfigSetCluster(rootSettings)) configRootCmd.AddCommand(NewCmdConfigGetCluster(rootSettings)) configRootCmd.AddCommand(NewCmdConfigSetContext(rootSettings)) configRootCmd.AddCommand(NewCmdConfigGetContext(rootSettings)) configRootCmd.AddCommand(NewCmdConfigInit(rootSettings)) + configRootCmd.AddCommand(NewCmdConfigSetAuthInfo(rootSettings)) + configRootCmd.AddCommand(NewCmdConfigGetAuthInfo(rootSettings)) return configRootCmd } diff --git a/cmd/config/get_authinfo.go b/cmd/config/get_authinfo.go new file mode 100644 index 000000000..4948feb68 --- /dev/null +++ b/cmd/config/get_authinfo.go @@ -0,0 +1,89 @@ +/* +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 ( + getAuthInfoLong = (`Display a specific user information, or all defined users if no name is provided`) + + getAuthInfoExample = (`# List all the users airshipctl knows about +airshipctl config get-credential + +# Display a specific user information +airshipctl config get-credential e2e`) +) + +// An AuthInfo refers to a particular user for a cluster +// NewCmdConfigGetAuthInfo returns a Command instance for 'config -AuthInfo' sub command +func NewCmdConfigGetAuthInfo(rootSettings *environment.AirshipCTLSettings) *cobra.Command { + theAuthInfo := &config.AuthInfoOptions{} + getauthinfocmd := &cobra.Command{ + Use: "get-credentials NAME", + Short: "Gets a user entry from the airshipctl config", + Long: getAuthInfoLong, + Example: getAuthInfoExample, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + theAuthInfo.Name = args[0] + } + return runGetAuthInfo(theAuthInfo, cmd.OutOrStdout(), rootSettings.Config()) + }, + } + + return getauthinfocmd +} + +// runGetAuthInfo performs the execution of 'config get-credentials' sub command +func runGetAuthInfo(o *config.AuthInfoOptions, out io.Writer, airconfig *config.Config) error { + if o.Name == "" { + return getAuthInfos(out, airconfig) + } + return getAuthInfo(o, out, airconfig) +} + +func getAuthInfo(o *config.AuthInfoOptions, out io.Writer, airconfig *config.Config) error { + cName := o.Name + authinfo, err := airconfig.GetAuthInfo(cName) + if err != nil { + return err + } + fmt.Fprintln(out, authinfo) + return nil +} + +func getAuthInfos(out io.Writer, airconfig *config.Config) error { + authinfos, err := airconfig.GetAuthInfos() + if err != nil { + return err + } + if len(authinfos) == 0 { + fmt.Fprintln(out, "No User credentials found in the configuration.") + } + for _, authinfo := range authinfos { + fmt.Fprintln(out, authinfo) + } + return nil +} diff --git a/cmd/config/get_authinfo_test.go b/cmd/config/get_authinfo_test.go new file mode 100644 index 000000000..d1cb21c0b --- /dev/null +++ b/cmd/config/get_authinfo_test.go @@ -0,0 +1,106 @@ +/* +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 ( + fooAuthInfo = "AuthInfoFoo" + barAuthInfo = "AuthInfoBar" + bazAuthInfo = "AuthInfoBaz" + missingAuthInfo = "authinfoMissing" +) + +func TestGetAuthInfoCmd(t *testing.T) { + conf := &config.Config{ + AuthInfos: map[string]*config.AuthInfo{ + fooAuthInfo: getTestAuthInfo(), + barAuthInfo: getTestAuthInfo(), + bazAuthInfo: getTestAuthInfo(), + }, + } + + settings := &environment.AirshipCTLSettings{} + settings.SetConfig(conf) + + cmdTests := []*testutil.CmdTest{ + { + Name: "get-credentials", + CmdLine: fmt.Sprintf("%s", fooAuthInfo), + Cmd: cmd.NewCmdConfigGetAuthInfo(settings), + }, + { + Name: "get-all-credentials", + CmdLine: fmt.Sprintf("%s %s", fooAuthInfo, barAuthInfo), + Cmd: cmd.NewCmdConfigGetAuthInfo(settings), + }, + // This is not implemented yet + { + Name: "get-multiple-credentials", + CmdLine: fmt.Sprintf("%s %s", fooAuthInfo, barAuthInfo), + Cmd: cmd.NewCmdConfigGetAuthInfo(settings), + }, + + { + Name: "missing", + CmdLine: fmt.Sprintf("%s", missingAuthInfo), + Cmd: cmd.NewCmdConfigGetAuthInfo(settings), + Error: fmt.Errorf("User %s information was not "+ + "found in the configuration.", missingAuthInfo), + }, + } + + for _, tt := range cmdTests { + testutil.RunTest(t, tt) + } +} + +func TestNoAuthInfosGetAuthInfoCmd(t *testing.T) { + settings := &environment.AirshipCTLSettings{} + settings.SetConfig(&config.Config{}) + cmdTest := &testutil.CmdTest{ + Name: "no-credentials", + CmdLine: "", + Cmd: cmd.NewCmdConfigGetAuthInfo(settings), + } + testutil.RunTest(t, cmdTest) +} + +func getTestAuthInfo() *config.AuthInfo { + kAuthInfo := &kubeconfig.AuthInfo{ + Username: "dummy_user", + Password: "dummy_password", + ClientCertificate: "dummy_certificate", + ClientKey: "dummy_key", + Token: "dummy_token", + } + + newAuthInfo := &config.AuthInfo{} + newAuthInfo.SetKubeAuthInfo(kAuthInfo) + + return newAuthInfo +} diff --git a/cmd/config/set_authinfo.go b/cmd/config/set_authinfo.go new file mode 100644 index 000000000..72a55f0df --- /dev/null +++ b/cmd/config/set_authinfo.go @@ -0,0 +1,162 @@ +/* +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" + "opendev.org/airship/airshipctl/pkg/log" +) + +var ( + setAuthInfoLong = fmt.Sprintf(`Sets a user entry in airshipctl config +Specifying a name that already exists will merge new fields on top of existing values. + +Client-certificate flags: +--%v=certfile --%v=keyfile + +Bearer token flags: +--%v=bearer_token + +Basic auth flags: +--%v=basic_user --%v=basic_password + +Bearer token and basic auth are mutually exclusive.`, + config.FlagCertFile, + config.FlagKeyFile, + config.FlagBearerToken, + config.FlagUsername, + config.FlagPassword) + + setAuthInfoExample = fmt.Sprintf(` +# Set only the "client-key" field on the "cluster-admin" +# entry, without touching other values: +airshipctl config set-credentials cluster-admin --%v=~/.kube/admin.key + +# Set basic auth for the "cluster-admin" entry +airshipctl config set-credentials cluster-admin --%v=admin --%v=uXFGweU9l35qcif + +# Embed client certificate data in the "cluster-admin" entry +airshipctl config set-credentials cluster-admin --%v=~/.kube/admin.crt --%v=true`, + config.FlagUsername, + config.FlagUsername, + config.FlagPassword, + config.FlagCertFile, + config.FlagEmbedCerts, + ) +) + +// NewCmdConfigSetAuthInfo creates a command object for the "set-credentials" action, which +// defines a new AuthInfo airship config. +func NewCmdConfigSetAuthInfo(rootSettings *environment.AirshipCTLSettings) *cobra.Command { + theAuthInfo := &config.AuthInfoOptions{} + + setauthinfo := &cobra.Command{ + Use: "set-credentials NAME", + Short: "Sets a user entry in the airshipctl config", + Long: setAuthInfoLong, + Example: setAuthInfoExample, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + theAuthInfo.Name = args[0] + modified, err := runSetAuthInfo(theAuthInfo, rootSettings.Config()) + if err != nil { + return err + } + if modified { + fmt.Fprintf(cmd.OutOrStdout(), "User information %q modified.\n", theAuthInfo.Name) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "User information %q created.\n", theAuthInfo.Name) + } + return nil + }, + } + + err := suInitFlags(theAuthInfo, setauthinfo) + if err != nil { + log.Fatal(err) + } + return setauthinfo +} + +func suInitFlags(o *config.AuthInfoOptions, setauthinfo *cobra.Command) error { + setauthinfo.Flags().StringVar(&o.ClientCertificate, config.FlagCertFile, o.ClientCertificate, + "Path to "+config.FlagCertFile+" file for the user entry in airshipctl") + err := setauthinfo.MarkFlagFilename(config.FlagCertFile) + if err != nil { + return err + } + + setauthinfo.Flags().StringVar(&o.ClientKey, config.FlagKeyFile, o.ClientKey, + "Path to "+config.FlagKeyFile+" file for the user entry in airshipctl") + err = setauthinfo.MarkFlagFilename(config.FlagKeyFile) + if err != nil { + return err + } + + setauthinfo.Flags().StringVar(&o.Token, config.FlagBearerToken, o.Token, + config.FlagBearerToken+" for the user entry in airshipctl") + + setauthinfo.Flags().StringVar(&o.Username, config.FlagUsername, o.Username, + config.FlagUsername+" for the user entry in airshipctl") + + setauthinfo.Flags().StringVar(&o.Password, config.FlagPassword, o.Password, + config.FlagPassword+" for the user entry in airshipctl") + + setauthinfo.Flags().BoolVar(&o.EmbedCertData, config.FlagEmbedCerts, false, + "Embed client cert/key for the user entry in airshipctl") + + return nil +} + +func runSetAuthInfo(o *config.AuthInfoOptions, airconfig *config.Config) (bool, error) { + authinfoWasModified := false + err := o.Validate() + if err != nil { + return authinfoWasModified, err + } + + authinfoIWant := o.Name + authinfo, err := airconfig.GetAuthInfo(authinfoIWant) + if err != nil { + var cerr config.ErrMissingConfig + if !errors.As(err, &cerr) { + // An error occurred, but it wasn't a "missing" config error. + return authinfoWasModified, err + } + + // authinfo didn't exist, create it + // ignoring the returned added authinfo + airconfig.AddAuthInfo(o) + } else { + // AuthInfo exists, lets update + airconfig.ModifyAuthInfo(authinfo, o) + authinfoWasModified = true + } + // Update configuration file just in time persistence approach + if err := airconfig.PersistConfig(); err != nil { + // Error that it didnt persist the changes + return authinfoWasModified, config.ErrConfigFailed{} + } + + return authinfoWasModified, nil +} diff --git a/cmd/config/set_authinfo_test.go b/cmd/config/set_authinfo_test.go new file mode 100644 index 000000000..457832dba --- /dev/null +++ b/cmd/config/set_authinfo_test.go @@ -0,0 +1,158 @@ +/* +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 ( + testUsername = "admin@kubernetes" + testPassword = "adminPassword" + testNewname = "dummy" + testOldname = "def-user" + pwdDelta = "_changed" +) + +type setAuthInfoTest struct { + description string + config *config.Config + args []string + flags []string + expected string + expectedConfig *config.Config +} + +func TestConfigSetAuthInfo(t *testing.T) { + cmdTests := []*testutil.CmdTest{ + { + Name: "config-cmd-set-authinfo-with-help", + CmdLine: "--help", + Cmd: NewCmdConfigSetAuthInfo(nil), + }, + } + + for _, tt := range cmdTests { + testutil.RunTest(t, tt) + } +} + +func initConfig(t *testing.T, withUser bool, testname string) (*config.Config, *config.Config) { + conf := config.InitConfig(t) + if withUser { + kAuthInfo := kubeconfig.NewAuthInfo() + kAuthInfo.Username = testUsername + kAuthInfo.Password = testPassword + conf.KubeConfig().AuthInfos[testname] = kAuthInfo + conf.AuthInfos[testname].SetKubeAuthInfo(kAuthInfo) + } + + expconf := config.InitConfig(t) + expconf.AuthInfos[testname] = config.NewAuthInfo() + + expkAuthInfo := kubeconfig.NewAuthInfo() + expkAuthInfo.Username = testUsername + expkAuthInfo.Password = testPassword + expconf.KubeConfig().AuthInfos[testname] = expkAuthInfo + expconf.AuthInfos[testname].SetKubeAuthInfo(expkAuthInfo) + + return conf, expconf +} + +func TestSetAuthInfo(t *testing.T) { + conf, expconf := initConfig(t, false, testNewname) + + test := setAuthInfoTest{ + description: "Testing 'airshipctl config set-credential' with a new user", + config: conf, + args: []string{testNewname}, + flags: []string{ + "--" + config.FlagUsername + "=" + testUsername, + "--" + config.FlagPassword + "=" + testPassword, + }, + expected: `User information "` + testNewname + `" created.` + "\n", + expectedConfig: expconf, + } + test.run(t) +} + +func TestModifyAuthInfo(t *testing.T) { + conf, expconf := initConfig(t, true, testOldname) + expconf.AuthInfos[testOldname].KubeAuthInfo().Password = testPassword + pwdDelta + + test := setAuthInfoTest{ + description: "Testing 'airshipctl config set-credential' with an existing user", + config: conf, + args: []string{testOldname}, + flags: []string{ + "--" + config.FlagPassword + "=" + testPassword + pwdDelta, + }, + expected: `User information "` + testOldname + `" modified.` + "\n", + expectedConfig: expconf, + } + test.run(t) +} + +func (test setAuthInfoTest) run(t *testing.T) { + // Get the Environment + settings := &environment.AirshipCTLSettings{} + settings.SetConfig(test.config) + + buf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdConfigSetAuthInfo(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 AuthInfo Created or Modified + afterRunAuthInfo, err := afterRunConf.GetAuthInfo(test.args[0]) + require.NoError(t, err) + require.NotNil(t, afterRunAuthInfo) + + afterKauthinfo := afterRunAuthInfo.KubeAuthInfo() + require.NotNil(t, afterKauthinfo) + + testKauthinfo := test.expectedConfig.KubeConfig().AuthInfos[test.args[0]] + require.NotNil(t, testKauthinfo) + + assert.EqualValues(t, testKauthinfo.Username, afterKauthinfo.Username) + assert.EqualValues(t, testKauthinfo.Password, afterKauthinfo.Password) + + // Test that the Return Message looks correct + if len(test.expected) != 0 { + assert.EqualValues(t, test.expected, buf.String()) + } +} diff --git a/cmd/config/set_cluster.go b/cmd/config/set_cluster.go index c974a35ca..730e6ac2e 100644 --- a/cmd/config/set_cluster.go +++ b/cmd/config/set_cluster.go @@ -56,7 +56,7 @@ airshipctl config set-cluster e2e --%v-type=target --%v=true --%v=".airship/cert ) // NewCmdConfigSetCluster creates a command object for the "set-cluster" action, which -// defines a new cluster airship config. +// defines a new cluster airshipctl config. func NewCmdConfigSetCluster(rootSettings *environment.AirshipCTLSettings) *cobra.Command { theCluster := &config.ClusterOptions{} diff --git a/cmd/config/set_context.go b/cmd/config/set_context.go index 38eab3161..bab38808c 100644 --- a/cmd/config/set_context.go +++ b/cmd/config/set_context.go @@ -46,7 +46,7 @@ airshipctl config set-context e2e --%v=true`, ) // NewCmdConfigSetContext creates a command object for the "set-context" action, which -// defines a new Context airship config. +// defines a new Context airshipctl config. func NewCmdConfigSetContext(rootSettings *environment.AirshipCTLSettings) *cobra.Command { theContext := &config.ContextOptions{} diff --git a/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden b/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden index 301af284c..2db28fab3 100644 --- a/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden +++ b/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-defaults.golden @@ -1,16 +1,18 @@ Modify airshipctl config files using subcommands -like "airshipctl config set-current-context my-context" +like "airshipctl config set-context --current-context my-context" Usage: config [command] Available Commands: - 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 - init Generate initial configuration files for airshipctl - set-cluster Sets a cluster entry in the airshipctl config - set-context Sets a context entry or updates current-context in the airshipctl config + 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 + get-credentials Gets a user entry from the airshipctl config + help Help about any command + init Generate initial configuration files for airshipctl + set-cluster Sets a cluster entry in the airshipctl config + set-context Sets a context entry or updates current-context in the airshipctl config + set-credentials Sets a user entry 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 301af284c..2db28fab3 100644 --- a/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-help.golden +++ b/cmd/config/testdata/TestConfigGoldenOutput/config-cmd-with-help.golden @@ -1,16 +1,18 @@ Modify airshipctl config files using subcommands -like "airshipctl config set-current-context my-context" +like "airshipctl config set-context --current-context my-context" Usage: config [command] Available Commands: - 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 - init Generate initial configuration files for airshipctl - set-cluster Sets a cluster entry in the airshipctl config - set-context Sets a context entry or updates current-context in the airshipctl config + 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 + get-credentials Gets a user entry from the airshipctl config + help Help about any command + init Generate initial configuration files for airshipctl + set-cluster Sets a cluster entry in the airshipctl config + set-context Sets a context entry or updates current-context in the airshipctl config + set-credentials Sets a user entry in the airshipctl config Flags: -h, --help help for config diff --git a/cmd/config/testdata/TestConfigSetAuthInfoGoldenOutput/config-cmd-set-authinfo-with-help.golden b/cmd/config/testdata/TestConfigSetAuthInfoGoldenOutput/config-cmd-set-authinfo-with-help.golden new file mode 100644 index 000000000..1fe9a1f65 --- /dev/null +++ b/cmd/config/testdata/TestConfigSetAuthInfoGoldenOutput/config-cmd-set-authinfo-with-help.golden @@ -0,0 +1,37 @@ +Sets a user entry in airshipctl config +Specifying a name that already exists will merge new fields on top of existing values. + +Client-certificate flags: +--client-certificate=certfile --client-key=keyfile + +Bearer token flags: +--token=bearer_token + +Basic auth flags: +--username=basic_user --password=basic_password + +Bearer token and basic auth are mutually exclusive. + +Usage: + set-credentials NAME [flags] + +Examples: + +# Set only the "client-key" field on the "cluster-admin" +# entry, without touching other values: +airshipctl config set-credentials cluster-admin --username=~/.kube/admin.key + +# Set basic auth for the "cluster-admin" entry +airshipctl config set-credentials cluster-admin --username=admin --password=uXFGweU9l35qcif + +# Embed client certificate data in the "cluster-admin" entry +airshipctl config set-credentials cluster-admin --client-certificate=~/.kube/admin.crt --embed-certs=true + +Flags: + --client-certificate string Path to client-certificate file for the user entry in airshipctl + --client-key string Path to client-key file for the user entry in airshipctl + --embed-certs Embed client cert/key for the user entry in airshipctl + -h, --help help for set-credentials + --password string password for the user entry in airshipctl + --token string token for the user entry in airshipctl + --username string username for the user entry in airshipctl diff --git a/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-all-credentials.golden b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-all-credentials.golden new file mode 100644 index 000000000..7c1cd2b08 --- /dev/null +++ b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-all-credentials.golden @@ -0,0 +1,21 @@ +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_user + +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_user + +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_user + diff --git a/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-credentials.golden b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-credentials.golden new file mode 100644 index 000000000..c85b2eab1 --- /dev/null +++ b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-credentials.golden @@ -0,0 +1,7 @@ +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_user + diff --git a/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-multiple-credentials.golden b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-multiple-credentials.golden new file mode 100644 index 000000000..7c1cd2b08 --- /dev/null +++ b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/get-multiple-credentials.golden @@ -0,0 +1,21 @@ +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_user + +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_user + +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_user + diff --git a/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/missing.golden b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/missing.golden new file mode 100644 index 000000000..f70d58032 --- /dev/null +++ b/cmd/config/testdata/TestGetAuthInfoCmdGoldenOutput/missing.golden @@ -0,0 +1,14 @@ +Error: Missing configuration: User credentials with name 'authinfoMissing' +Usage: + get-credentials NAME [flags] + +Examples: +# List all the users airshipctl knows about +airshipctl config get-credential + +# Display a specific user information +airshipctl config get-credential e2e + +Flags: + -h, --help help for get-credentials + diff --git a/cmd/config/testdata/TestNoAuthInfosGetAuthInfoCmdGoldenOutput/no-credentials.golden b/cmd/config/testdata/TestNoAuthInfosGetAuthInfoCmdGoldenOutput/no-credentials.golden new file mode 100644 index 000000000..872f61b10 --- /dev/null +++ b/cmd/config/testdata/TestNoAuthInfosGetAuthInfoCmdGoldenOutput/no-credentials.golden @@ -0,0 +1 @@ +No User credentials found in the configuration. diff --git a/pkg/config/cmds.go b/pkg/config/cmds.go index 5cf769393..a22b96933 100644 --- a/pkg/config/cmds.go +++ b/pkg/config/cmds.go @@ -62,3 +62,28 @@ func (o *ContextOptions) Validate() error { // TODO Manifest, Cluster could be validated against the existing config maps return nil } + +func (o *AuthInfoOptions) Validate() error { + if len(o.Token) > 0 && (len(o.Username) > 0 || len(o.Password) > 0) { + return fmt.Errorf("you cannot specify more than one authentication method at the same time: --%v or --%v/--%v", + FlagBearerToken, FlagUsername, FlagPassword) + } + if !o.EmbedCertData { + return nil + } + certPath := o.ClientCertificate + if certPath == "" { + return fmt.Errorf("you must specify a --%s to embed", FlagCertFile) + } + if _, err := ioutil.ReadFile(certPath); err != nil { + return fmt.Errorf("error reading %s data from %s: %v", FlagCertFile, certPath, err) + } + keyPath := o.ClientKey + if keyPath == "" { + return fmt.Errorf("you must specify a --%s to embed", FlagKeyFile) + } + if _, err := ioutil.ReadFile(keyPath); err != nil { + return fmt.Errorf("error reading %s data from %s: %v", FlagKeyFile, keyPath, err) + } + return nil +} diff --git a/pkg/config/cmds_test.go b/pkg/config/cmds_test.go index b5a9ef9d9..50896012c 100644 --- a/pkg/config/cmds_test.go +++ b/pkg/config/cmds_test.go @@ -68,3 +68,15 @@ func TestValidateContext(t *testing.T) { err := co.Validate() assert.NoError(t, err) } + +func TestValidateAuthInfo(t *testing.T) { + co := DummyAuthInfoOptions() + // Token and cert error case + err := co.Validate() + assert.Error(t, err) + + // Valid Data case + co.Token = "" + err = co.Validate() + assert.NoError(t, err) +} diff --git a/pkg/config/cmds_types.go b/pkg/config/cmds_types.go index a6190ebc9..367a60ef7 100644 --- a/pkg/config/cmds_types.go +++ b/pkg/config/cmds_types.go @@ -18,3 +18,13 @@ type ContextOptions struct { Manifest string Namespace string } + +type AuthInfoOptions struct { + Name string + ClientCertificate string + ClientKey string + Token string + Username string + Password string + EmbedCertData bool +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 4f90a0751..f5d52a09c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -240,6 +240,7 @@ func (c *Config) reconcileAuthInfos() { // Add the reference c.AuthInfos[key] = NewAuthInfo() } + c.AuthInfos[key].SetKubeAuthInfo(authinfo) } // Checking if there is any AuthInfo reference in airship config that does not match // an actual Auth Info struct in kubeconfig @@ -619,6 +620,61 @@ func (c *Config) CurrentContextManifest() (*Manifest, error) { return c.Manifests[currentContext.Manifest], nil } +// Credential or AuthInfo related methods +func (c *Config) GetAuthInfo(aiName string) (*AuthInfo, error) { + authinfo, exists := c.AuthInfos[aiName] + if !exists { + return nil, ErrMissingConfig{What: fmt.Sprintf("User credentials with name '%s'", aiName)} + } + return authinfo, nil +} + +func (c *Config) GetAuthInfos() ([]*AuthInfo, error) { + authinfos := []*AuthInfo{} + for cName := range c.AuthInfos { + authinfo, err := c.GetAuthInfo(cName) + if err == nil { + authinfos = append(authinfos, authinfo) + } + } + return authinfos, nil +} + +func (c *Config) AddAuthInfo(theAuthInfo *AuthInfoOptions) *AuthInfo { + // Create the new Airship config context + nAuthInfo := NewAuthInfo() + c.AuthInfos[theAuthInfo.Name] = nAuthInfo + // Create a new Kubeconfig AuthInfo object as well + kAuthInfo := kubeconfig.NewAuthInfo() + nAuthInfo.SetKubeAuthInfo(kAuthInfo) + c.KubeConfig().AuthInfos[theAuthInfo.Name] = kAuthInfo + + c.ModifyAuthInfo(nAuthInfo, theAuthInfo) + return nAuthInfo +} + +func (c *Config) ModifyAuthInfo(authinfo *AuthInfo, theAuthInfo *AuthInfoOptions) { + kAuthInfo := authinfo.KubeAuthInfo() + if kAuthInfo == nil { + return + } + if theAuthInfo.ClientCertificate != "" { + kAuthInfo.ClientCertificate = theAuthInfo.ClientCertificate + } + if theAuthInfo.Token != "" { + kAuthInfo.Token = theAuthInfo.Token + } + if theAuthInfo.Username != "" { + kAuthInfo.Username = theAuthInfo.Username + } + if theAuthInfo.Password != "" { + kAuthInfo.Password = theAuthInfo.Password + } + if theAuthInfo.ClientKey != "" { + kAuthInfo.ClientKey = theAuthInfo.ClientKey + } +} + // CurrentContextBootstrapInfo returns bootstrap info for current context func (c *Config) CurrentContextBootstrapInfo() (*Bootstrap, error) { currentCluster, err := c.CurrentContextCluster() @@ -738,17 +794,25 @@ func (c *Context) ClusterType() string { // AuthInfo functions func (c *AuthInfo) Equal(d *AuthInfo) bool { if d == nil { - return d == c + return c == d } - return c == d + return c.kAuthInfo == d.kAuthInfo } func (c *AuthInfo) String() string { - yaml, err := yaml.Marshal(&c) + kauthinfo := c.KubeAuthInfo() + kyaml, err := yaml.Marshal(&kauthinfo) if err != nil { return "" } - return string(yaml) + return string(kyaml) +} + +func (c *AuthInfo) KubeAuthInfo() *kubeconfig.AuthInfo { + return c.kAuthInfo +} +func (c *AuthInfo) SetKubeAuthInfo(kc *kubeconfig.AuthInfo) { + c.kAuthInfo = kc } // Manifest functions @@ -931,3 +995,11 @@ func KContextString(kContext *kubeconfig.Context) string { return string(yaml) } +func KAuthInfoString(kAuthInfo *kubeconfig.AuthInfo) string { + yaml, err := yaml.Marshal(&kAuthInfo) + if err != nil { + return "" + } + + return string(yaml) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 0536d559c..cc0c5dd4d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -32,6 +32,8 @@ import ( "opendev.org/airship/airshipctl/testutil" ) +const stringDelta = "_changed" + func TestString(t *testing.T) { fSys := testutil.SetupTestFs(t, "testdata") @@ -215,7 +217,7 @@ func TestLoadConfig(t *testing.T) { require.Contains(t, conf.Clusters, "def") assert.Len(t, conf.Clusters["def"].ClusterTypes, 2) assert.Len(t, conf.Contexts, 3) - assert.Len(t, conf.AuthInfos, 2) + assert.Len(t, conf.AuthInfos, 3) } func TestPersistConfig(t *testing.T) { @@ -385,6 +387,15 @@ func TestKContextString(t *testing.T) { } assert.EqualValues(t, KClusterString(nil), "null\n") } +func TestKAuthInfoString(t *testing.T) { + conf := InitConfig(t) + kAuthInfos := conf.KubeConfig().AuthInfos + for kAi := range kAuthInfos { + assert.NotEmpty(t, KAuthInfoString(kAuthInfos[kAi])) + } + assert.EqualValues(t, KAuthInfoString(nil), "null\n") +} + func TestComplexName(t *testing.T) { cName := "aCluster" ctName := Ephemeral @@ -434,13 +445,13 @@ func TestAddCluster(t *testing.T) { assert.EqualValues(t, conf.Clusters[co.Name].ClusterTypes[co.ClusterType], cluster) } -func TestModifyluster(t *testing.T) { +func TestModifyCluster(t *testing.T) { co := DummyClusterOptions() conf := InitConfig(t) cluster, err := conf.AddCluster(co) require.NoError(t, err) - co.Server += "/changes" + co.Server += stringDelta co.InsecureSkipTLSVerify = true co.EmbedCAData = true mcluster, err := conf.ModifyCluster(cluster, co) @@ -533,3 +544,75 @@ func TestGetContext(t *testing.T) { _, err = conf.GetContext("unknown") assert.Error(t, err) } + +func TestAddContext(t *testing.T) { + co := DummyContextOptions() + conf := InitConfig(t) + context := conf.AddContext(co) + assert.EqualValues(t, conf.Contexts[co.Name], context) +} + +func TestModifyContext(t *testing.T) { + co := DummyContextOptions() + conf := InitConfig(t) + context := conf.AddContext(co) + + co.Namespace += stringDelta + co.Cluster += stringDelta + co.AuthInfo += stringDelta + co.Manifest += stringDelta + conf.ModifyContext(context, co) + assert.EqualValues(t, conf.Contexts[co.Name].KubeContext().Namespace, co.Namespace) + assert.EqualValues(t, conf.Contexts[co.Name].KubeContext().Cluster, co.Cluster) + assert.EqualValues(t, conf.Contexts[co.Name].KubeContext().AuthInfo, co.AuthInfo) + assert.EqualValues(t, conf.Contexts[co.Name].Manifest, co.Manifest) + assert.EqualValues(t, conf.Contexts[co.Name], context) +} + +// AuthInfo Related + +func TestGetAuthInfos(t *testing.T) { + conf := InitConfig(t) + authinfos, err := conf.GetAuthInfos() + require.NoError(t, err) + assert.Len(t, authinfos, 3) +} + +func TestGetAuthInfo(t *testing.T) { + conf := InitConfig(t) + authinfo, err := conf.GetAuthInfo("def-user") + require.NoError(t, err) + + // Test Positives + assert.EqualValues(t, authinfo.KubeAuthInfo().Username, "dummy_username") + + // Test Wrong Cluster + _, err = conf.GetAuthInfo("unknown") + assert.Error(t, err) +} + +func TestAddAuthInfo(t *testing.T) { + co := DummyAuthInfoOptions() + conf := InitConfig(t) + authinfo := conf.AddAuthInfo(co) + assert.EqualValues(t, conf.AuthInfos[co.Name], authinfo) +} + +func TestModifyAuthInfo(t *testing.T) { + co := DummyAuthInfoOptions() + conf := InitConfig(t) + authinfo := conf.AddAuthInfo(co) + + co.Username += stringDelta + co.Password += stringDelta + co.ClientCertificate += stringDelta + co.ClientKey += stringDelta + co.Token += stringDelta + conf.ModifyAuthInfo(authinfo, co) + assert.EqualValues(t, conf.AuthInfos[co.Name].KubeAuthInfo().Username, co.Username) + assert.EqualValues(t, conf.AuthInfos[co.Name].KubeAuthInfo().Password, co.Password) + assert.EqualValues(t, conf.AuthInfos[co.Name].KubeAuthInfo().ClientCertificate, co.ClientCertificate) + assert.EqualValues(t, conf.AuthInfos[co.Name].KubeAuthInfo().ClientKey, co.ClientKey) + assert.EqualValues(t, conf.AuthInfos[co.Name].KubeAuthInfo().Token, co.Token) + assert.EqualValues(t, conf.AuthInfos[co.Name], authinfo) +} diff --git a/pkg/config/test_utils.go b/pkg/config/test_utils.go index 6455557b4..3b1463d6c 100644 --- a/pkg/config/test_utils.go +++ b/pkg/config/test_utils.go @@ -104,7 +104,15 @@ func DummyRepository() *Repository { } func DummyAuthInfo() *AuthInfo { - return NewAuthInfo() + a := NewAuthInfo() + authinfo := kubeconfig.NewAuthInfo() + authinfo.Username = "dummy_username" + authinfo.Password = "dummy_password" + authinfo.ClientCertificate = "dummy_certificate" + authinfo.ClientKey = "dummy_key" + authinfo.Token = "dummy_token" + a.SetKubeAuthInfo(authinfo) + return a } func DummyModules() *Modules { @@ -168,6 +176,16 @@ func DummyContextOptions() *ContextOptions { return co } +func DummyAuthInfoOptions() *AuthInfoOptions { + authinfo := &AuthInfoOptions{} + authinfo.Username = "dummy_username" + authinfo.Password = "dummy_password" + authinfo.ClientCertificate = "dummy_certificate" + authinfo.ClientKey = "dummy_key" + authinfo.Token = "dummy_token" + return authinfo +} + func DummyBootstrap() *Bootstrap { bs := &Bootstrap{} cont := Container{ @@ -224,7 +242,8 @@ modules-config: dummy-for-tests: "" users: k-admin: {} - k-other: {}` + k-other: {} + def-user: {}` //nolint:lll testKubeConfigYAML = `apiVersion: v1 @@ -262,6 +281,11 @@ current-context: "" kind: Config preferences: {} users: +users: +- name: def-user + user: + username: dummy_username + password: dummy_password - name: k-admin user: client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJQXhEdzk2RUY4SXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RBNU1qa3hOekF6TURsYUZ3MHlNREE1TWpneE56QXpNVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXV6R0pZdlBaNkRvaTQyMUQKSzhXSmFaQ25OQWQycXo1cC8wNDJvRnpRUGJyQWd6RTJxWVZrek9MOHhBVmVSN1NONXdXb1RXRXlGOEVWN3JyLwo0K0hoSEdpcTVQbXF1SUZ5enpuNi9JWmM4alU5eEVmenZpa2NpckxmVTR2UlhKUXdWd2dBU05sMkFXQUloMmRECmRUcmpCQ2ZpS1dNSHlqMFJiSGFsc0J6T3BnVC9IVHYzR1F6blVRekZLdjJkajVWMU5rUy9ESGp5UlJKK0VMNlEKQlltR3NlZzVQNE5iQzllYnVpcG1NVEFxL0p1bU9vb2QrRmpMMm5acUw2Zkk2ZkJ0RjVPR2xwQ0IxWUo4ZnpDdApHUVFaN0hUSWJkYjJ0cDQzRlZPaHlRYlZjSHFUQTA0UEoxNSswV0F5bVVKVXo4WEE1NDRyL2J2NzRKY0pVUkZoCmFyWmlRd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMMmhIUmVibEl2VHJTMFNmUVg1RG9ueVVhNy84aTg1endVWApSd3dqdzFuS0U0NDJKbWZWRGZ5b0hRYUM4Ti9MQkxyUXM0U0lqU1JYdmFHU1dSQnRnT1RRV21Db1laMXdSbjdwCndDTXZQTERJdHNWWm90SEZpUFl2b1lHWFFUSXA3YlROMmg1OEJaaEZ3d25nWUovT04zeG1rd29IN1IxYmVxWEYKWHF1TTluekhESk41VlZub1lQR09yRHMwWlg1RnNxNGtWVU0wVExNQm9qN1ZIRDhmU0E5RjRYNU4yMldsZnNPMAo4aksrRFJDWTAyaHBrYTZQQ0pQS0lNOEJaMUFSMG9ZakZxT0plcXpPTjBqcnpYWHh4S2pHVFVUb1BldVA5dCtCCjJOMVA1TnI4a2oxM0lrend5Q1NZclFVN09ZM3ltZmJobHkrcXZxaFVFa014MlQ1SkpmQT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= diff --git a/pkg/config/testdata/authinfo-string.yaml b/pkg/config/testdata/authinfo-string.yaml index 0967ef424..7169895b9 100644 --- a/pkg/config/testdata/authinfo-string.yaml +++ b/pkg/config/testdata/authinfo-string.yaml @@ -1 +1,6 @@ -{} +LocationOfOrigin: "" +client-certificate: dummy_certificate +client-key: dummy_key +password: dummy_password +token: dummy_token +username: dummy_username diff --git a/pkg/config/types.go b/pkg/config/types.go index c9b9dc9d4..69d87b8ec 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -112,8 +112,8 @@ type Context struct { } type AuthInfo struct { - // Empty in purpose - // Will implement Interface to Set/Get fields from kubeconfig as needed + // Kubeconfig AuthInfo Object + kAuthInfo *kubeconfig.AuthInfo } // Manifests is a tuple of references to a Manifest (how do Identify, collect ,