diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 119d5e765..a38d9d5f0 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -16,25 +16,15 @@ package main import ( + "fmt" "os" - "github.com/go-logr/zapr" - ctrlruntimelog "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/percona/everest/commands" - "github.com/percona/everest/pkg/logger" ) func main() { - l := logger.MustInitLogger(false) - - // This is required because controller-runtime requires a logger - // to be set within 30 seconds of the program initialization. - log := zapr.NewLogger(l) - ctrlruntimelog.SetLogger(log) - - rootCmd := commands.NewRootCmd(l.Sugar()) - if err := rootCmd.Execute(); err != nil { + if err := commands.Execute(); err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } } diff --git a/cmd/main.go b/cmd/main.go index 402a2dbe7..a1873711c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,7 +37,7 @@ const ( ) func main() { - logger := logger.MustInitLogger(true) + logger := logger.MustInitLogger(true, "everest") defer logger.Sync() //nolint:errcheck l := logger.Sugar() diff --git a/commands/accounts.go b/commands/accounts.go index f4539960f..d15eb8e58 100644 --- a/commands/accounts.go +++ b/commands/accounts.go @@ -18,24 +18,25 @@ package commands import ( "github.com/spf13/cobra" - "go.uber.org/zap" "github.com/percona/everest/commands/accounts" ) -func newAccountsCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "accounts", - Long: "Manage Everest accounts", - Short: "Manage Everest accounts", - } +var accountsCmd = &cobra.Command{ + Use: "accounts [flags]", + Args: cobra.ExactArgs(1), + Long: "Manage Everest accounts", + Short: "Manage Everest accounts", + Run: func(_ *cobra.Command, _ []string) {}, +} - cmd.AddCommand(accounts.NewCreateCmd(l)) - cmd.AddCommand(accounts.NewListCmd(l)) - cmd.AddCommand(accounts.NewDeleteCmd(l)) - cmd.AddCommand(accounts.NewSetPwCommand(l)) - cmd.AddCommand(accounts.NewResetJWTKeysCommand(l)) - cmd.AddCommand(accounts.NewInitialAdminPasswdCommand(l)) +func init() { + rootCmd.AddCommand(accountsCmd) - return cmd + accountsCmd.AddCommand(accounts.GetCreateCmd()) + accountsCmd.AddCommand(accounts.GetListCmd()) + accountsCmd.AddCommand(accounts.GetDeleteCmd()) + accountsCmd.AddCommand(accounts.GetSetPasswordCmd()) + accountsCmd.AddCommand(accounts.GetResetJWTKeysCmd()) + accountsCmd.AddCommand(accounts.GetInitAdminPasswordCmd()) } diff --git a/commands/accounts/create.go b/commands/accounts/create.go index cfc76f8db..d7c276ffb 100644 --- a/commands/accounts/create.go +++ b/commands/accounts/create.go @@ -19,64 +19,56 @@ package accounts import ( - "context" - "errors" - "net/url" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" accountscli "github.com/percona/everest/pkg/accounts/cli" - "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/cli" + "github.com/percona/everest/pkg/logger" ) -// NewCreateCmd returns a new create command. -func NewCreateCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "create", +var ( + accountsCreateCmd = &cobra.Command{ + Use: "create [flags]", + Args: cobra.NoArgs, Example: "everestctl accounts create --username user1 --password $USER_PASS", Short: "Create a new Everest user account", Long: "Create a new Everest user account", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initCreateViperFlags(cmd) + PreRun: accountsCreatePreRun, + Run: accountsCreateRun, + } - kubeconfigPath := viper.GetString("kubeconfig") - username := viper.GetString("username") - password := viper.GetString("password") + accountsCreateCfg = &accountscli.Config{} + accountsCreateOpts = &accountscli.CreateOptions{} +) - k, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) { - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - os.Exit(0) - } +func init() { + // local command flags + accountsCreateCmd.Flags().StringVarP(&accountsCreateOpts.Username, cli.FlagAccountsUsername, "u", "", "Username of the account") + accountsCreateCmd.Flags().StringVarP(&accountsCreateOpts.Password, cli.FlagAccountsCreatePassword, "p", "", "Password of the account") +} - cli := accountscli.New(l) - cli.WithAccountManager(k.Accounts()) +func accountsCreatePreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + accountsCreateCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + accountsCreateCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} - if err := cli.Create(context.Background(), username, password); err != nil { - l.Error(err) - os.Exit(1) - } - }, +func accountsCreateRun(cmd *cobra.Command, _ []string) { //nolint:revive + cliA, err := accountscli.NewAccounts(*accountsCreateCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) } - initCreateFlags(cmd) - return cmd -} -func initCreateFlags(cmd *cobra.Command) { - cmd.Flags().StringP("username", "u", "", "Username of the account") - cmd.Flags().StringP("password", "p", "", "Password of the account") + if err := cliA.Create(cmd.Context(), *accountsCreateOpts); err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } } -func initCreateViperFlags(cmd *cobra.Command) { - viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec - viper.BindPFlag("password", cmd.Flags().Lookup("password")) //nolint:errcheck,gosec - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec +// GetCreateCmd returns the command to create a new user account. +func GetCreateCmd() *cobra.Command { + return accountsCreateCmd } diff --git a/commands/accounts/delete.go b/commands/accounts/delete.go index dfdf5c5df..6c2929630 100644 --- a/commands/accounts/delete.go +++ b/commands/accounts/delete.go @@ -17,61 +17,54 @@ package accounts import ( - "context" - "errors" - "net/url" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" accountscli "github.com/percona/everest/pkg/accounts/cli" - "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/cli" + "github.com/percona/everest/pkg/logger" ) -// NewDeleteCmd returns a new delete command. -func NewDeleteCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "delete", +var ( + accountsDeleteCmd = &cobra.Command{ + Use: "delete [flags]", + Args: cobra.NoArgs, Example: "everestctl accounts delete --username user1", Short: "Delete an existing Everest user account", Long: "Delete an existing Everest user account", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initDeleteViperFlags(cmd) - - kubeconfigPath := viper.GetString("kubeconfig") - username := viper.GetString("username") + PreRun: accountsDeletePreRun, + Run: accountsDeleteRun, + } + accountsDeleteCfg = &accountscli.Config{} + accountsDeleteUsername string +) - k, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) { - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - os.Exit(0) - } +func init() { + // local command flags + accountsDeleteCmd.Flags().StringVarP(&accountsDeleteUsername, cli.FlagAccountsUsername, "u", "", "Username of the account") +} - cli := accountscli.New(l) - cli.WithAccountManager(k.Accounts()) +func accountsDeletePreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + accountsDeleteCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + accountsDeleteCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} - if err := cli.Delete(context.Background(), username); err != nil { - l.Error(err) - os.Exit(1) - } - }, +func accountsDeleteRun(cmd *cobra.Command, _ []string) { //nolint:revive + cliA, err := accountscli.NewAccounts(*accountsDeleteCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) } - initDeleteFlags(cmd) - return cmd -} -func initDeleteFlags(cmd *cobra.Command) { - cmd.Flags().StringP("username", "u", "", "Username of the account") + if err := cliA.Delete(cmd.Context(), accountsDeleteUsername); err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } } -func initDeleteViperFlags(cmd *cobra.Command) { - viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec +// GetDeleteCmd returns the command to delete account. +func GetDeleteCmd() *cobra.Command { + return accountsDeleteCmd } diff --git a/commands/accounts/initial_admin_password.go b/commands/accounts/initial_admin_password.go index 1e3521b88..c57039be0 100644 --- a/commands/accounts/initial_admin_password.go +++ b/commands/accounts/initial_admin_password.go @@ -1,61 +1,68 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 accounts holds commands for accounts command. package accounts import ( - "context" - "errors" "fmt" - "net/url" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" - "github.com/percona/everest/pkg/common" - "github.com/percona/everest/pkg/kubernetes" + accountscli "github.com/percona/everest/pkg/accounts/cli" + "github.com/percona/everest/pkg/cli" + "github.com/percona/everest/pkg/logger" ) -// NewInitialAdminPasswdCommand returns a new initial-admin-passwd command. -func NewInitialAdminPasswdCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "initial-admin-password", +var ( + accountsInitAdminPasswdCmd = &cobra.Command{ + Use: "initial-admin-password [flags]", + Args: cobra.NoArgs, Example: "everestctl accounts initial-admin-password", Long: "Get the initial admin password for Everest", Short: "Get the initial admin password for Everest", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec - kubeconfigPath := viper.GetString("kubeconfig") - - k, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - if !errors.As(err, &u) { - l.Error(err) - } - os.Exit(1) - } - - ctx := context.Background() - secure, err := k.Accounts().IsSecure(ctx, common.EverestAdminUser) - if err != nil { - l.Error(err) - os.Exit(1) - } - if secure { - l.Error("Cannot retrieve admin password after it has been updated.") - os.Exit(1) - } - admin, err := k.Accounts().Get(ctx, common.EverestAdminUser) - if err != nil { - l.Error(err) - os.Exit(1) - } - fmt.Fprint(os.Stdout, admin.PasswordHash+"\n") - }, + PreRun: accountsInitAdminPasswdPreRun, + Run: accountsInitAdminPasswdRun, + } + accountsInitAdminPasswdCfg = &accountscli.Config{} +) + +func accountsInitAdminPasswdPreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + accountsInitAdminPasswdCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + accountsInitAdminPasswdCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} + +func accountsInitAdminPasswdRun(cmd *cobra.Command, _ []string) { //nolint:revive + cliA, err := accountscli.NewAccounts(*accountsInitAdminPasswdCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) } - return cmd + + passwordHash, err := cliA.GetInitAdminPassword(cmd.Context()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } + + _, _ = fmt.Fprint(os.Stdout, passwordHash+"\n") +} + +// GetInitAdminPasswordCmd returns the command to get the initial admin password. +func GetInitAdminPasswordCmd() *cobra.Command { + return accountsInitAdminPasswdCmd } diff --git a/commands/accounts/list.go b/commands/accounts/list.go index c1ed09e04..475b53a6a 100644 --- a/commands/accounts/list.go +++ b/commands/accounts/list.go @@ -17,66 +17,60 @@ package accounts import ( - "context" - "errors" - "net/url" + "fmt" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" accountscli "github.com/percona/everest/pkg/accounts/cli" - "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/cli" + "github.com/percona/everest/pkg/logger" ) -// NewListCmd returns a new list command. -func NewListCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "list", - Example: "everestctl accounts list", +var ( + accountsListCmd = &cobra.Command{ + Use: "list [flags]", + Args: cobra.NoArgs, + Example: "everestctl accounts list --no-headers", Long: "List all Everest user accounts", Short: "List all Everest user accounts", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initListViperFlags(cmd) - o := &accountscli.ListOptions{} - err := viper.Unmarshal(o) - if err != nil { - os.Exit(1) - } - kubeconfigPath := viper.GetString("kubeconfig") + PreRun: accountsListPreRun, + Run: accountsListRun, + } + accountsListCfg = &accountscli.Config{} + accountsListOpts = &accountscli.ListOptions{} +) - k, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) { - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - os.Exit(0) - } +func init() { + // local command flags + accountsListCmd.Flags().BoolVar(&accountsListOpts.NoHeaders, "no-headers", false, "If set, hide table headers") + accountsListCmd.Flags().StringSliceVar(&accountsListOpts.Columns, "columns", nil, + fmt.Sprintf("Comma-separated list of column names to display. Supported columns: %s, %s, %s.", + accountscli.ColumnUser, accountscli.ColumnCapabilities, accountscli.ColumnEnabled, + ), + ) +} - cli := accountscli.New(l) - cli.WithAccountManager(k.Accounts()) +func accountsListPreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + accountsListCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + accountsListCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} - if err := cli.List(context.Background(), o); err != nil { - l.Error(err) - os.Exit(1) - } - }, +func accountsListRun(cmd *cobra.Command, _ []string) { //nolint:revive + cliA, err := accountscli.NewAccounts(*accountsListCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) } - initListFlags(cmd) - return cmd -} -func initListFlags(cmd *cobra.Command) { - cmd.Flags().Bool("no-headers", false, "If set, hide table headers") - cmd.Flags().StringSlice("columns", nil, "Comma-separated list of column names to display") + if err := cliA.List(cmd.Context(), *accountsListOpts); err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } } -func initListViperFlags(cmd *cobra.Command) { - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec - viper.BindPFlag("no-headers", cmd.Flags().Lookup("no-headers")) //nolint:errcheck,gosec - viper.BindPFlag("columns", cmd.Flags().Lookup("columns")) //nolint:errcheck,gosec +// GetListCmd returns the command to list all user accounts. +func GetListCmd() *cobra.Command { + return accountsListCmd } diff --git a/commands/accounts/reset_jwt_keys.go b/commands/accounts/reset_jwt_keys.go index 34fd7791f..1cf94be3c 100644 --- a/commands/accounts/reset_jwt_keys.go +++ b/commands/accounts/reset_jwt_keys.go @@ -1,50 +1,64 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 accounts holds commands for accounts command. package accounts import ( - "context" - "errors" - "net/url" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" - "github.com/percona/everest/pkg/kubernetes" + accountscli "github.com/percona/everest/pkg/accounts/cli" + "github.com/percona/everest/pkg/cli" + "github.com/percona/everest/pkg/logger" ) -// NewResetJWTKeysCommand returns a new reset-jwt-keys command. -func NewResetJWTKeysCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "reset-jwt-keys", +var ( + accountsResetJWTKeysCmd = &cobra.Command{ + Use: "reset-jwt-keys [flags]", + Args: cobra.NoArgs, Example: "everestctl accounts reset-jwt-keys", Long: "Reset the JWT keys used for Everest user authentication", Short: "Reset the JWT keys used for Everest user authentication", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec - kubeconfigPath := viper.GetString("kubeconfig") - - k, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) { - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - os.Exit(1) - } - - ctx := context.Background() - l.Info("Creating/Updating JWT keys and restarting Everest..") - if err := k.CreateRSAKeyPair(ctx); err != nil { - l.Error(err) - os.Exit(1) - } - - l.Info("JWT keys created/updated successfully!") - }, + PreRun: accountsResetJWTKeysPreRun, + Run: accountsResetJWTKeysRun, + } + accountsResetJWTKeysCfg = &accountscli.Config{} +) + +func accountsResetJWTKeysPreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + accountsResetJWTKeysCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + accountsResetJWTKeysCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} + +func accountsResetJWTKeysRun(cmd *cobra.Command, _ []string) { //nolint:revive + cliA, err := accountscli.NewAccounts(*accountsResetJWTKeysCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } + + if err := cliA.CreateRSAKeyPair(cmd.Context()); err != nil { + logger.GetLogger().Error(err) + os.Exit(1) } - return cmd +} + +// GetResetJWTKeysCmd returns the command to reset the JWT keys used for Everest user authentication. +func GetResetJWTKeysCmd() *cobra.Command { + return accountsResetJWTKeysCmd } diff --git a/commands/accounts/set_password.go b/commands/accounts/set_password.go index 0e0ee814b..05ac7a500 100644 --- a/commands/accounts/set_password.go +++ b/commands/accounts/set_password.go @@ -1,67 +1,73 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 accounts holds commands for accounts command. // //nolint:dupl package accounts import ( - "context" - "errors" - "net/url" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" accountscli "github.com/percona/everest/pkg/accounts/cli" - "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/cli" + "github.com/percona/everest/pkg/logger" ) -// NewSetPwCommand returns a new set-password command. -func NewSetPwCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "set-password", +var ( + accountsSetPasswordCmd = &cobra.Command{ + Use: "set-password [flags]", + Args: cobra.NoArgs, Example: "everestctl accounts set-password --username user1 --new-password $USER_PASS", Long: "Set a new password for an existing Everest user account", Short: "Set a new password for an existing Everest user account", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initSetPwViperFlags(cmd) - - kubeconfigPath := viper.GetString("kubeconfig") - username := viper.GetString("username") - password := viper.GetString("new-password") + PreRun: accountsSetPasswordPreRun, + Run: accountsSetPasswordRun, + } + accountsSetPasswordCfg = &accountscli.Config{} + accountsSetPasswordOpts = &accountscli.SetPasswordOptions{} +) - k, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) { - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - os.Exit(0) - } +func init() { + // local command flags + accountsSetPasswordCmd.Flags().StringVarP(&accountsSetPasswordOpts.Username, cli.FlagAccountsUsername, "u", "", "Username of the account") + accountsSetPasswordCmd.Flags().StringVarP(&accountsSetPasswordOpts.NewPassword, cli.FlagAccountsNewPassword, "p", "", "New password for the account") +} - cli := accountscli.New(l) - cli.WithAccountManager(k.Accounts()) +func accountsSetPasswordPreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + accountsSetPasswordCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + accountsSetPasswordCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} - if err := cli.SetPassword(context.Background(), username, password); err != nil { - l.Error(err) - os.Exit(1) - } - }, +func accountsSetPasswordRun(cmd *cobra.Command, _ []string) { //nolint:revive + cliA, err := accountscli.NewAccounts(*accountsSetPasswordCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) } - initSetPwFlags(cmd) - return cmd -} -func initSetPwFlags(cmd *cobra.Command) { - cmd.Flags().StringP("username", "u", "", "Username of the account") - cmd.Flags().StringP("new-password", "p", "", "New password for the account") + if err := cliA.SetPassword(cmd.Context(), *accountsSetPasswordOpts); err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } } -func initSetPwViperFlags(cmd *cobra.Command) { - viper.BindPFlag("username", cmd.Flags().Lookup("username")) //nolint:errcheck,gosec - viper.BindPFlag("new-password", cmd.Flags().Lookup("new-password")) //nolint:errcheck,gosec - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec +// GetSetPasswordCmd returns the command to set password for an account. +func GetSetPasswordCmd() *cobra.Command { + return accountsSetPasswordCmd } diff --git a/commands/install.go b/commands/install.go index 38780ba2e..90a10bbc4 100644 --- a/commands/install.go +++ b/commands/install.go @@ -20,106 +20,91 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/cli/helm" "github.com/percona/everest/pkg/cli/install" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/output" ) -func newInstallCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "install", +var ( + installCmd = &cobra.Command{ + Use: "install [flags]", // The command expects no arguments. So to prevent users from misspelling and confusion // in cases with unexpected spaces like // ./everestctl install --namespaces=aaa, a // it will return // Error: unknown command "a" for "everestctl install" Args: cobra.NoArgs, - Example: "everestctl install --namespaces dev,staging,prod --operator.mongodb=true --operator.postgresql=true --operator.xtradb-cluster=true --skip-wizard", + Example: "everestctl install --namespaces dev,staging,prod --operator.mongodb=true --operator.postgresql=false --operator.xtradb-cluster=false --skip-wizard", Long: "Install Percona Everest using Helm", Short: "Install Percona Everest using Helm", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initInstallViperFlags(cmd) - c := &install.Config{} - err := viper.Unmarshal(c) - if err != nil { - os.Exit(1) - } - c.CLIOptions.BindViperFlags() - - if c.SkipDBNamespace { - if cmd.Flags().Lookup(cli.FlagNamespaces).Changed { - l.Errorf("cannot set both --%s and --%s", cli.FlagInstallSkipDBNamespace, cli.FlagNamespaces) - os.Exit(1) - } - c.Namespaces = "" - } - - enableLogging := viper.GetBool("verbose") || viper.GetBool("json") - c.Pretty = !enableLogging - - op, err := install.NewInstall(*c, l, cmd) - if err != nil { - l.Error(err) - os.Exit(1) - } - - if err := op.Run(cmd.Context()); err != nil { - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - }, + PreRunE: installPreRunE, + Run: installRun, } - initInstallFlags(cmd) + installCfg = &install.Config{} +) - return cmd +func init() { + rootCmd.AddCommand(installCmd) + + // local command flags + installCmd.Flags().StringVar(&installCfg.Namespaces, cli.FlagNamespaces, install.DefaultDBNamespaceName, "Comma-separated namespaces list Percona Everest can manage") + installCmd.Flags().BoolVar(&installCfg.SkipWizard, cli.FlagSkipWizard, false, "Skip installation wizard") + installCmd.Flags().StringVar(&installCfg.VersionMetadataURL, cli.FlagVersionMetadataURL, "https://check.percona.com", "URL to retrieve version metadata information from") + installCmd.Flags().StringVar(&installCfg.Version, cli.FlagVersion, "", "Everest version to install. By default the latest version is installed") + installCmd.Flags().BoolVar(&installCfg.DisableTelemetry, cli.FlagDisableTelemetry, false, "Disable telemetry") + _ = installCmd.Flags().MarkHidden(cli.FlagDisableTelemetry) + installCmd.Flags().BoolVar(&installCfg.SkipEnvDetection, cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") + installCmd.Flags().BoolVar(&installCfg.SkipDBNamespace, cli.FlagInstallSkipDBNamespace, false, "Skip creating a database namespace with install") + + // --namespaces and --skip-db-namespace flags are mutually exclusive + installCmd.MarkFlagsMutuallyExclusive(cli.FlagNamespaces, cli.FlagInstallSkipDBNamespace) + + // --helm.* flags + installCmd.Flags().StringVar(&installCfg.ChartDir, helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") + _ = installCmd.Flags().MarkHidden(helm.FlagChartDir) + installCmd.Flags().StringVar(&installCfg.RepoURL, helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") + installCmd.Flags().StringSliceVar(&installCfg.Values.Values, helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") + installCmd.Flags().StringSliceVarP(&installCfg.Values.ValueFiles, helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") + + // --operator.* flags + installCmd.Flags().BoolVar(&installCfg.Operator.PSMDB, cli.FlagOperatorMongoDB, true, "Install MongoDB operator") + installCmd.Flags().BoolVar(&installCfg.Operator.PG, cli.FlagOperatorPostgresql, true, "Install PostgreSQL operator") + installCmd.Flags().BoolVar(&installCfg.Operator.PXC, cli.FlagOperatorXtraDBCluster, true, "Install XtraDB Cluster operator") } -func initInstallFlags(cmd *cobra.Command) { - cmd.Flags().String(cli.FlagNamespaces, install.DefaultDBNamespaceName, "Comma-separated namespaces list Percona Everest can manage") - cmd.Flags().Bool(cli.FlagSkipWizard, false, "Skip installation wizard") - cmd.Flags().String(cli.FlagVersionMetadataURL, "https://check.percona.com", "URL to retrieve version metadata information from") - cmd.Flags().String(cli.FlagVersion, "", "Everest version to install. By default the latest version is installed") - cmd.Flags().Bool(cli.FlagDisableTelemetry, false, "Disable telemetry") - cmd.Flags().MarkHidden(cli.FlagDisableTelemetry) //nolint:errcheck,gosec - cmd.Flags().Bool(cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") - cmd.Flags().Bool(cli.FlagInstallSkipDBNamespace, false, "Skip creating a database namespace with install") - - cmd.Flags().String(helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") - cmd.Flags().MarkHidden(helm.FlagChartDir) //nolint:errcheck,gosec - cmd.Flags().String(helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") - cmd.Flags().StringSlice(helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") - cmd.Flags().StringSliceP(helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") - - cmd.Flags().Bool(cli.FlagOperatorMongoDB, true, "Install MongoDB operator") - cmd.Flags().Bool(cli.FlagOperatorPostgresql, true, "Install PostgreSQL operator") - cmd.Flags().Bool(cli.FlagOperatorXtraDBCluster, true, "Install XtraDB Cluster operator") +func installPreRunE(cmd *cobra.Command, _ []string) error { //nolint:revive + if installCfg.SkipDBNamespace { + installCfg.Namespaces = "" + } + + // Copy global flags to config + installCfg.Pretty = rootCmdFlags.Pretty + installCfg.KubeconfigPath = rootCmdFlags.KubeconfigPath + + // If user doesn't pass --namespaces flag - need to ask explicitly. + installCfg.AskNamespaces = !(cmd.Flags().Lookup(cli.FlagNamespaces).Changed || installCfg.SkipDBNamespace) + + // If user doesn't pass any --operator.* flags - need to ask explicitly. + installCfg.AskOperators = !(cmd.Flags().Lookup(cli.FlagOperatorMongoDB).Changed || + cmd.Flags().Lookup(cli.FlagOperatorPostgresql).Changed || + cmd.Flags().Lookup(cli.FlagOperatorXtraDBCluster).Changed || + installCfg.SkipDBNamespace) + + return nil } -func initInstallViperFlags(cmd *cobra.Command) { - viper.BindPFlag(cli.FlagSkipWizard, cmd.Flags().Lookup(cli.FlagSkipWizard)) //nolint:errcheck,gosec - - viper.BindEnv(cli.FlagKubeconfig) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagKubeconfig, cmd.Flags().Lookup(cli.FlagKubeconfig)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagNamespaces, cmd.Flags().Lookup(cli.FlagNamespaces)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagVersionMetadataURL, cmd.Flags().Lookup(cli.FlagVersionMetadataURL)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagVersion, cmd.Flags().Lookup(cli.FlagVersion)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagDisableTelemetry, cmd.Flags().Lookup(cli.FlagDisableTelemetry)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagSkipEnvDetection, cmd.Flags().Lookup(cli.FlagSkipEnvDetection)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagInstallSkipDBNamespace, cmd.Flags().Lookup(cli.FlagInstallSkipDBNamespace)) //nolint:errcheck,gosec - - viper.BindPFlag(helm.FlagChartDir, cmd.Flags().Lookup(helm.FlagChartDir)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagRepository, cmd.Flags().Lookup(helm.FlagRepository)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmSet, cmd.Flags().Lookup(helm.FlagHelmSet)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmValues, cmd.Flags().Lookup(helm.FlagHelmValues)) //nolint:errcheck,gosec - - viper.BindPFlag(cli.FlagOperatorMongoDB, cmd.Flags().Lookup(cli.FlagOperatorMongoDB)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagOperatorPostgresql, cmd.Flags().Lookup(cli.FlagOperatorPostgresql)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagOperatorXtraDBCluster, cmd.Flags().Lookup(cli.FlagOperatorXtraDBCluster)) //nolint:errcheck,gosec - - viper.BindPFlag(cli.FlagVerbose, cmd.Flags().Lookup(cli.FlagVerbose)) //nolint:errcheck,gosec - viper.BindPFlag("json", cmd.Flags().Lookup("json")) //nolint:errcheck,gosec +func installRun(cmd *cobra.Command, _ []string) { //nolint:revive + op, err := install.NewInstall(*installCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } + + if err := op.Run(cmd.Context()); err != nil { + output.PrintError(err, logger.GetLogger(), installCfg.Pretty) + os.Exit(1) + } } diff --git a/commands/namespaces.go b/commands/namespaces.go index 9452328dd..4804e979c 100644 --- a/commands/namespaces.go +++ b/commands/namespaces.go @@ -18,19 +18,22 @@ package commands import ( "github.com/spf13/cobra" - "go.uber.org/zap" "github.com/percona/everest/commands/namespaces" ) -func newNamespacesCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "namespaces", - Long: "Managed Everest database namespaces", - Short: "Managed Everest database namespaces", - } - cmd.AddCommand(namespaces.NewAddCommand(l)) - cmd.AddCommand(namespaces.NewRemoveCommand(l)) - cmd.AddCommand(namespaces.NewUpdateCommand(l)) - return cmd +var namespacesCmd = &cobra.Command{ + Use: "namespaces [flags]", + Args: cobra.ExactArgs(1), + Long: "Managed Everest database namespaces", + Short: "Managed Everest database namespaces", + Run: func(_ *cobra.Command, _ []string) {}, +} + +func init() { + rootCmd.AddCommand(namespacesCmd) + + namespacesCmd.AddCommand(namespaces.GetNamespacesAddCmd()) + namespacesCmd.AddCommand(namespaces.GetNamespacesRemoveCmd()) + namespacesCmd.AddCommand(namespaces.GetNamespacesUpdateCmd()) } diff --git a/commands/namespaces/add.go b/commands/namespaces/add.go index 6aca77ed3..5f57e8bfe 100644 --- a/commands/namespaces/add.go +++ b/commands/namespaces/add.go @@ -1,3 +1,18 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 namespaces provides the namespaces CLI command. package namespaces @@ -7,12 +22,11 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/cli/helm" "github.com/percona/everest/pkg/cli/namespaces" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/output" ) @@ -20,102 +34,72 @@ import ( var ( takeOwnershipHintMessage = fmt.Sprintf("HINT: set '--%s' flag to use existing namespaces", cli.FlagTakeNamespaceOwnership) updateHintMessage = "HINT: use 'everestctl namespaces update' to update the namespace" -) - -// NewAddCommand returns a new command to add a new namespace. -func NewAddCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "add [flags] NAMESPACES", - Long: "Add a new namespace", - Short: "Add a new namespace", - Example: `everestctl namespaces add --operator.mongodb=true --operator.postgresql=false --operator.xtradb-cluster=false --skip-wizard ns-1,ns-2`, + namespacesAddCmd = &cobra.Command{ + Use: "add [flags]", Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - initAddViperFlags(cmd) - c := &namespaces.NamespaceAddConfig{} - err := viper.Unmarshal(c) - if err != nil { - l.Error(err) - return - } - bindInstallHelmOpts(c) - - c.Namespaces = args[0] - enableLogging := viper.GetBool("verbose") || viper.GetBool("json") - c.Pretty = !enableLogging - - askOperators := !(cmd.Flags().Lookup("operator.mongodb").Changed || - cmd.Flags().Lookup("operator.postgresql").Changed || - cmd.Flags().Lookup("operator.xtradb-cluster").Changed) - - if err := c.Populate(cmd.Context(), false, askOperators); err != nil { - if errors.Is(err, namespaces.ErrNamespaceAlreadyExists) { - err = fmt.Errorf("%w. %s", err, takeOwnershipHintMessage) - } - if errors.Is(err, namespaces.ErrNamespaceAlreadyOwned) { - err = fmt.Errorf("%w. %s", err, updateHintMessage) - } - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - - op, err := namespaces.NewNamespaceAdd(*c, l) - if err != nil { - output.PrintError(err, l, !enableLogging) - return - } - if err := op.Run(cmd.Context()); err != nil { - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - }, + Long: "Add a new namespace and make managed by Everest", + Short: "Add a new namespace and make managed by Everest", + Example: `everestctl namespaces add ns-1,ns-2 --skip-wizard --operator.xtradb-cluster=true --operator.postgresql=false --operator.mongodb=false`, + PreRun: namespacesAddPreRun, + Run: namespacesAddRun, } - initAddFlags(cmd) - return cmd + namespacesAddCfg = &namespaces.NamespaceAddConfig{} +) + +func init() { + // local command flags + namespacesAddCmd.Flags().BoolVar(&namespacesAddCfg.DisableTelemetry, cli.FlagDisableTelemetry, false, "Disable telemetry") + _ = namespacesAddCmd.Flags().MarkHidden(cli.FlagDisableTelemetry) //nolint:errcheck,gosec + namespacesAddCmd.Flags().BoolVar(&namespacesAddCfg.SkipWizard, cli.FlagSkipWizard, false, "Skip installation wizard") + namespacesAddCmd.Flags().BoolVar(&namespacesAddCfg.TakeOwnership, cli.FlagTakeNamespaceOwnership, false, "If the specified namespace already exists, take ownership of it") + namespacesAddCmd.Flags().BoolVar(&namespacesAddCfg.SkipEnvDetection, cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") + + // --helm.* flags + namespacesAddCmd.Flags().StringVar(&namespacesAddCfg.CLIOptions.ChartDir, helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") + _ = namespacesAddCmd.Flags().MarkHidden(helm.FlagChartDir) //nolint:errcheck,gosec + namespacesAddCmd.Flags().StringVar(&namespacesAddCfg.CLIOptions.RepoURL, helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") + namespacesAddCmd.Flags().StringSliceVar(&namespacesAddCfg.CLIOptions.Values.Values, helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") + namespacesAddCmd.Flags().StringSliceVarP(&namespacesAddCfg.CLIOptions.Values.ValueFiles, helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") + + // --operator.* flags + namespacesAddCmd.Flags().BoolVar(&namespacesAddCfg.Operator.PSMDB, cli.FlagOperatorMongoDB, true, "Install MongoDB operator") + namespacesAddCmd.Flags().BoolVar(&namespacesAddCfg.Operator.PG, cli.FlagOperatorPostgresql, true, "Install PostgreSQL operator") + namespacesAddCmd.Flags().BoolVar(&namespacesAddCfg.Operator.PXC, cli.FlagOperatorXtraDBCluster, true, "Install XtraDB Cluster operator") } -func initAddFlags(cmd *cobra.Command) { - cmd.Flags().Bool(cli.FlagDisableTelemetry, false, "Disable telemetry") - cmd.Flags().MarkHidden(cli.FlagDisableTelemetry) //nolint:errcheck,gosec - cmd.Flags().Bool(cli.FlagSkipWizard, false, "Skip installation wizard") - cmd.Flags().Bool(cli.FlagTakeNamespaceOwnership, false, "If the specified namespace already exists, take ownership of it") - cmd.Flags().Bool(cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") +func namespacesAddPreRun(cmd *cobra.Command, args []string) { //nolint:revive + namespacesAddCfg.Namespaces = args[0] - cmd.Flags().String(helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") - cmd.Flags().MarkHidden(helm.FlagChartDir) //nolint:errcheck,gosec - cmd.Flags().String(helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") - cmd.Flags().StringSlice(helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") - cmd.Flags().StringSliceP(helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") + // Copy global flags to config + namespacesAddCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + namespacesAddCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() - cmd.Flags().Bool(cli.FlagOperatorMongoDB, true, "Install MongoDB operator") - cmd.Flags().Bool(cli.FlagOperatorPostgresql, true, "Install PostgreSQL operator") - cmd.Flags().Bool(cli.FlagOperatorXtraDBCluster, true, "Install XtraDB Cluster operator") + // If user doesn't pass any --operator.* flags - need to ask explicitly. + namespacesAddCfg.AskOperators = !(cmd.Flags().Lookup(cli.FlagOperatorMongoDB).Changed || + cmd.Flags().Lookup(cli.FlagOperatorPostgresql).Changed || + cmd.Flags().Lookup(cli.FlagOperatorXtraDBCluster).Changed) } -func initAddViperFlags(cmd *cobra.Command) { - viper.BindPFlag(cli.FlagSkipWizard, cmd.Flags().Lookup(cli.FlagSkipWizard)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagDisableTelemetry, cmd.Flags().Lookup(cli.FlagDisableTelemetry)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagTakeNamespaceOwnership, cmd.Flags().Lookup(cli.FlagTakeNamespaceOwnership)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagSkipEnvDetection, cmd.Flags().Lookup(cli.FlagSkipEnvDetection)) //nolint:errcheck,gosec - - viper.BindPFlag(helm.FlagChartDir, cmd.Flags().Lookup(helm.FlagChartDir)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagRepository, cmd.Flags().Lookup(helm.FlagRepository)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmSet, cmd.Flags().Lookup(helm.FlagHelmSet)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmValues, cmd.Flags().Lookup(helm.FlagHelmValues)) //nolint:errcheck,gosec - - viper.BindPFlag(cli.FlagOperatorMongoDB, cmd.Flags().Lookup("operator.mongodb")) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagOperatorPostgresql, cmd.Flags().Lookup("operator.postgresql")) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagOperatorXtraDBCluster, cmd.Flags().Lookup("operator.xtradb-cluster")) //nolint:errcheck,gosec +func namespacesAddRun(cmd *cobra.Command, _ []string) { + op, err := namespaces.NewNamespaceAdd(*namespacesAddCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } - viper.BindEnv(cli.FlagKubeconfig) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagKubeconfig, cmd.Flags().Lookup(cli.FlagKubeconfig)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagVerbose, cmd.Flags().Lookup(cli.FlagVerbose)) //nolint:errcheck,gosec - viper.BindPFlag("json", cmd.Flags().Lookup("json")) //nolint:errcheck,gosec + if err := op.Run(cmd.Context()); err != nil { + if errors.Is(err, namespaces.ErrNamespaceAlreadyExists) { + err = fmt.Errorf("%w. %s", err, takeOwnershipHintMessage) + } + if errors.Is(err, namespaces.ErrNamespaceAlreadyOwned) { + err = fmt.Errorf("%w. %s", err, updateHintMessage) + } + output.PrintError(err, logger.GetLogger(), namespacesAddCfg.Pretty) + os.Exit(1) + } } -func bindInstallHelmOpts(cfg *namespaces.NamespaceAddConfig) { - cfg.CLIOptions.Values.Values = viper.GetStringSlice(helm.FlagHelmSet) - cfg.CLIOptions.Values.ValueFiles = viper.GetStringSlice(helm.FlagHelmValues) - cfg.CLIOptions.ChartDir = viper.GetString(helm.FlagChartDir) - cfg.CLIOptions.RepoURL = viper.GetString(helm.FlagRepository) +// GetNamespacesAddCmd returns the command to add namespaces. +func GetNamespacesAddCmd() *cobra.Command { + return namespacesAddCmd } diff --git a/commands/namespaces/remove.go b/commands/namespaces/remove.go index 0b94e1425..4a00ff0ed 100644 --- a/commands/namespaces/remove.go +++ b/commands/namespaces/remove.go @@ -1,3 +1,18 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 namespaces provides the namespaces CLI command. package namespaces @@ -7,72 +22,59 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/cli/namespaces" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/output" ) const forceUninstallHint = "HINT: use --force to remove the namespace and all its resources" -// NewRemoveCommand returns a new command to remove an existing namespace. -func NewRemoveCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "remove [flags] NAMESPACES", - Long: "Remove existing and managed by Everest namespaces", - Short: "Remove existing and managed by Everest namespaces", - Example: `everestctl namespaces remove --keep-namespace --force ns-1,ns-2`, +var ( + namespacesRemoveCmd = &cobra.Command{ + Use: "remove [flags]", Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - initRemoveViperFlags(cmd) - c := &namespaces.NamespaceRemoveConfig{} - err := viper.Unmarshal(c) - if err != nil { - l.Error(err) - return - } - c.Namespaces = args[0] + Long: "Remove an existing namespace", + Short: "Remove an existing namespace", + Example: `everestctl namespaces remove ns-1,ns-2 --keep-namespace`, + PreRun: namespacesRemovePreRun, + Run: namespacesRemoveRun, + } + namespacesRemoveCfg = &namespaces.NamespaceRemoveConfig{} +) - enableLogging := viper.GetBool("verbose") || viper.GetBool("json") - c.Pretty = !enableLogging +func init() { + // local command flags + namespacesRemoveCmd.Flags().BoolVar(&namespacesRemoveCfg.KeepNamespace, cli.FlagKeepNamespace, false, "If set, preserves the Kubernetes namespace but removes all resources managed by Everest") + namespacesRemoveCmd.Flags().BoolVar(&namespacesRemoveCfg.Force, cli.FlagNamespaceForce, false, "If set, forcefully deletes database clusters in the namespace (if any)") +} - if err := c.Populate(cmd.Context()); err != nil { - if errors.Is(err, namespaces.ErrNamespaceNotEmpty) { - err = fmt.Errorf("%w. %s", err, forceUninstallHint) - } - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } +func namespacesRemovePreRun(cmd *cobra.Command, args []string) { //nolint:revive + namespacesRemoveCfg.Namespaces = args[0] - op, err := namespaces.NewNamespaceRemove(*c, l) - if err != nil { - output.PrintError(err, l, !enableLogging) - return - } + // Copy global flags to config + namespacesRemoveCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + namespacesRemoveCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} - if err := op.Run(cmd.Context()); err != nil { - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - }, +func namespacesRemoveRun(cmd *cobra.Command, _ []string) { + op, err := namespaces.NewNamespaceRemove(*namespacesRemoveCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) } - initRemoveFlags(cmd) - return cmd -} -func initRemoveFlags(cmd *cobra.Command) { - cmd.Flags().Bool("keep-namespace", false, "If set, preserves the Kubernetes namespace but removes all resources managed by Everest") - cmd.Flags().Bool("force", false, "If set, forcefully deletes database clusters in the namespace (if any)") + if err := op.Run(cmd.Context()); err != nil { + if errors.Is(err, namespaces.ErrNamespaceNotEmpty) { + err = fmt.Errorf("%w. %s", err, forceUninstallHint) + } + output.PrintError(err, logger.GetLogger(), namespacesRemoveCfg.Pretty) + os.Exit(1) + } } -func initRemoveViperFlags(cmd *cobra.Command) { - viper.BindPFlag("keep-namespace", cmd.Flags().Lookup("keep-namespace")) //nolint:errcheck,gosec - viper.BindPFlag("force", cmd.Flags().Lookup("force")) //nolint:errcheck,gosec - - viper.BindEnv(cli.FlagKubeconfig) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagKubeconfig, cmd.Flags().Lookup(cli.FlagKubeconfig)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagVerbose, cmd.Flags().Lookup(cli.FlagVerbose)) //nolint:errcheck,gosec - viper.BindPFlag("json", cmd.Flags().Lookup("json")) //nolint:errcheck,gosec +// GetNamespacesRemoveCmd returns the command to remove a namespaces. +func GetNamespacesRemoveCmd() *cobra.Command { + return namespacesRemoveCmd } diff --git a/commands/namespaces/update.go b/commands/namespaces/update.go index 1105594d6..b30cd879e 100644 --- a/commands/namespaces/update.go +++ b/commands/namespaces/update.go @@ -1,3 +1,18 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 namespaces provides the namespaces CLI command. package namespaces @@ -7,100 +22,83 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/cli/helm" "github.com/percona/everest/pkg/cli/namespaces" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/output" ) -// NewUpdateCommand returns a new command to update an existing namespace. -func NewUpdateCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "update [flags] NAMESPACES", +var ( + namespacesUpdateCmd = &cobra.Command{ + Use: "update [flags] ", + Args: cobra.ExactArgs(1), Long: "Add database operator to existing namespace managed by Everest", Short: "Add database operator to existing namespace managed by Everest", - Example: `everestctl namespaces update --operator.mongodb=true --skip-wizard ns-1,ns-2`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - initUpdateViperFlags(cmd) - c := &namespaces.NamespaceAddConfig{} - err := viper.Unmarshal(c) - if err != nil { - l.Error(err) - return - } - bindInstallHelmOpts(c) - c.Update = true - - c.Namespaces = args[0] - - enableLogging := viper.GetBool("verbose") || viper.GetBool("json") - c.Pretty = !enableLogging - - askOperators := !(cmd.Flags().Lookup("operator.mongodb").Changed || - cmd.Flags().Lookup("operator.postgresql").Changed || - cmd.Flags().Lookup("operator.xtradb-cluster").Changed) - - if err := c.Populate(cmd.Context(), false, askOperators); err != nil { - if errors.Is(err, namespaces.ErrNamespaceNotManagedByEverest) { - err = fmt.Errorf("%w. HINT: use 'everestctl namespaces add --%s %s' first to make namespace managed by Everest", - err, - cli.FlagTakeNamespaceOwnership, - c.Namespaces) - } - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - - op, err := namespaces.NewNamespaceAdd(*c, l) - if err != nil { - output.PrintError(err, l, !enableLogging) - return - } - if err := op.Run(cmd.Context()); err != nil { - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - }, + Example: `everestctl namespaces update ns-1,ns-2 --skip-wizard --operator.xtradb-cluster=true --operator.postgresql=false --operator.mongodb=false`, + PreRun: namespacesUpdatePreRun, + Run: namespacesUpdateRun, } - initUpdateFlags(cmd) - return cmd + namespacesUpdateCfg = &namespaces.NamespaceAddConfig{} +) + +func init() { + // local command flags + namespacesUpdateCmd.Flags().BoolVar(&namespacesUpdateCfg.DisableTelemetry, cli.FlagDisableTelemetry, false, "Disable telemetry") + _ = namespacesUpdateCmd.Flags().MarkHidden(cli.FlagDisableTelemetry) //nolint:errcheck,gosec + namespacesUpdateCmd.Flags().BoolVar(&namespacesUpdateCfg.SkipWizard, cli.FlagSkipWizard, false, "Skip installation wizard") + namespacesUpdateCmd.Flags().BoolVar(&namespacesUpdateCfg.SkipEnvDetection, cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") + + // --helm.* flags + namespacesUpdateCmd.Flags().StringVar(&namespacesUpdateCfg.CLIOptions.ChartDir, helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") + _ = namespacesUpdateCmd.Flags().MarkHidden(helm.FlagChartDir) //nolint:errcheck,gosec + namespacesUpdateCmd.Flags().StringVar(&namespacesUpdateCfg.CLIOptions.RepoURL, helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") + namespacesUpdateCmd.Flags().StringSliceVar(&namespacesUpdateCfg.CLIOptions.Values.Values, helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") + namespacesUpdateCmd.Flags().StringSliceVarP(&namespacesUpdateCfg.CLIOptions.Values.ValueFiles, helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") + + // --operator.* flags + namespacesUpdateCmd.Flags().BoolVar(&namespacesUpdateCfg.Operator.PSMDB, cli.FlagOperatorMongoDB, true, "Install MongoDB operator") + namespacesUpdateCmd.Flags().BoolVar(&namespacesUpdateCfg.Operator.PG, cli.FlagOperatorPostgresql, true, "Install PostgreSQL operator") + namespacesUpdateCmd.Flags().BoolVar(&namespacesUpdateCfg.Operator.PXC, cli.FlagOperatorXtraDBCluster, true, "Install XtraDB Cluster operator") } -func initUpdateFlags(cmd *cobra.Command) { - cmd.Flags().Bool(cli.FlagDisableTelemetry, false, "Disable telemetry") - cmd.Flags().MarkHidden(cli.FlagDisableTelemetry) //nolint:errcheck,gosec - cmd.Flags().Bool(cli.FlagSkipWizard, false, "Skip installation wizard") +func namespacesUpdatePreRun(cmd *cobra.Command, args []string) { //nolint:revive + namespacesUpdateCfg.Namespaces = args[0] + namespacesUpdateCfg.Update = true - cmd.Flags().String(helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") - cmd.Flags().MarkHidden(helm.FlagChartDir) //nolint:errcheck,gosec - cmd.Flags().String(helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") - cmd.Flags().StringSlice(helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") - cmd.Flags().StringSliceP(helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") + // Copy global flags to config + namespacesUpdateCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + namespacesUpdateCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() - cmd.Flags().Bool(cli.FlagOperatorMongoDB, true, "Install MongoDB operator") - cmd.Flags().Bool(cli.FlagOperatorPostgresql, true, "Install PostgreSQL operator") - cmd.Flags().Bool(cli.FlagOperatorXtraDBCluster, true, "Install XtraDB Cluster operator") + // If user doesn't pass any --operator.* flags - need to ask explicitly. + namespacesUpdateCfg.AskOperators = !(cmd.Flags().Lookup(cli.FlagOperatorMongoDB).Changed || + cmd.Flags().Lookup(cli.FlagOperatorPostgresql).Changed || + cmd.Flags().Lookup(cli.FlagOperatorXtraDBCluster).Changed) } -func initUpdateViperFlags(cmd *cobra.Command) { - viper.BindPFlag(cli.FlagSkipWizard, cmd.Flags().Lookup(cli.FlagSkipWizard)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagDisableTelemetry, cmd.Flags().Lookup(cli.FlagDisableTelemetry)) //nolint:errcheck,gosec - - viper.BindPFlag(helm.FlagChartDir, cmd.Flags().Lookup(helm.FlagChartDir)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagRepository, cmd.Flags().Lookup(helm.FlagRepository)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmSet, cmd.Flags().Lookup(helm.FlagHelmSet)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmValues, cmd.Flags().Lookup(helm.FlagHelmValues)) //nolint:errcheck,gosec +func namespacesUpdateRun(cmd *cobra.Command, _ []string) { + op, err := namespaces.NewNamespaceAdd(*namespacesUpdateCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } - viper.BindPFlag(cli.FlagOperatorMongoDB, cmd.Flags().Lookup("operator.mongodb")) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagOperatorPostgresql, cmd.Flags().Lookup("operator.postgresql")) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagOperatorXtraDBCluster, cmd.Flags().Lookup("operator.xtradb-cluster")) //nolint:errcheck,gosec + if err := op.Run(cmd.Context()); err != nil { + if errors.Is(err, namespaces.ErrNamespaceNotManagedByEverest) { + err = fmt.Errorf("%w. HINT: use 'everestctl namespaces add --%s %s' first to make namespace managed by Everest", + err, + cli.FlagTakeNamespaceOwnership, + namespacesUpdateCfg.Namespaces, + ) + } + + output.PrintError(err, logger.GetLogger(), namespacesAddCfg.Pretty) + os.Exit(1) + } +} - viper.BindEnv(cli.FlagKubeconfig) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagKubeconfig, cmd.Flags().Lookup(cli.FlagKubeconfig)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagVerbose, cmd.Flags().Lookup(cli.FlagVerbose)) //nolint:errcheck,gosec - viper.BindPFlag("json", cmd.Flags().Lookup("json")) //nolint:errcheck,gosec +// GetNamespacesUpdateCmd returns the command to update namespaces. +func GetNamespacesUpdateCmd() *cobra.Command { + return namespacesUpdateCmd } diff --git a/commands/root.go b/commands/root.go index 05db349c9..35b849cce 100644 --- a/commands/root.go +++ b/commands/root.go @@ -17,35 +17,59 @@ package commands import ( + "github.com/go-logr/zapr" + "github.com/kelseyhightower/envconfig" "github.com/spf13/cobra" - "go.uber.org/zap" + ctrlruntimelog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/logger" ) -// NewRootCmd creates a new root command for the cli. -func NewRootCmd(l *zap.SugaredLogger) *cobra.Command { - rootCmd := &cobra.Command{ - Use: "everestctl", - Long: "CLI for managing Percona Everest", - Short: "CLI for managing Percona Everest", - PersistentPreRun: func(cmd *cobra.Command, args []string) { //nolint:revive - logger.InitLoggerInRootCmd(cmd, l) - l.Debug("Debug logging enabled") - }, +type ( + globalFlags struct { + Verbose bool // Enable Verbose mode + JSON bool // Set output type to JSON + // If set, we will print the Pretty output. + Pretty bool + // Path to a kubeconfig + KubeconfigPath string `default:"~/.kube/config" envconfig:"KUBECONFIG"` } +) - rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose mode") - rootCmd.PersistentFlags().Bool("json", false, "Set output type to JSON") - rootCmd.PersistentFlags().StringP("kubeconfig", "k", "~/.kube/config", "Path to a kubeconfig") +var ( + rootCmd = &cobra.Command{ + Use: "everestctl [flags]", + Long: "CLI for managing Percona Everest", + Short: "CLI for managing Percona Everest", + PersistentPreRun: rootPersistentPreRun, + } + // Contains global variables applied to all commands and subcommands. + rootCmdFlags = &globalFlags{} +) - rootCmd.AddCommand(newInstallCmd(l)) - rootCmd.AddCommand(newVersionCmd(l)) - rootCmd.AddCommand(newUpgradeCmd(l)) - rootCmd.AddCommand(newUninstallCmd(l)) - rootCmd.AddCommand(newAccountsCmd(l)) - rootCmd.AddCommand(newSettingsCommand(l)) - rootCmd.AddCommand(newNamespacesCommand(l)) +func init() { + rootCmd.PersistentFlags().BoolVarP(&rootCmdFlags.Verbose, cli.FlagVerbose, "v", false, "Enable Verbose mode") + rootCmd.PersistentFlags().BoolVar(&rootCmdFlags.JSON, cli.FlagJSON, false, "Set output type to JSON") + + // Read KUBECONFIG ENV var first. + _ = envconfig.Process("", rootCmdFlags) + // if kubeconfig is passed explicitly via CLI - use it instead of ENV var. + rootCmd.PersistentFlags().StringVarP(&rootCmdFlags.KubeconfigPath, cli.FlagKubeconfig, "k", rootCmdFlags.KubeconfigPath, "Path to a kubeconfig. If not set, will use KUBECONFIG env var") +} + +func rootPersistentPreRun(_ *cobra.Command, _ []string) { //nolint:revive + logger.InitLoggerInRootCmd(rootCmdFlags.Verbose, rootCmdFlags.JSON, "everestctl") + + // This is required because controller-runtime requires a logger + // to be set within 30 seconds of the program initialization. + ctrlruntimelog.SetLogger(zapr.NewLogger(logger.GetLogger().Desugar())) + + rootCmdFlags.Pretty = !(rootCmdFlags.Verbose || rootCmdFlags.JSON) + logger.GetLogger().Debug("Debug logging enabled") +} - return rootCmd +// Execute executes the root command. +func Execute() error { + return rootCmd.Execute() } diff --git a/commands/settings.go b/commands/settings.go index 629f0ec1e..c34dac5b3 100644 --- a/commands/settings.go +++ b/commands/settings.go @@ -18,18 +18,21 @@ package commands import ( "github.com/spf13/cobra" - "go.uber.org/zap" "github.com/percona/everest/commands/settings" ) -func newSettingsCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "settings", - Long: "Configure Everest settings", - Short: "Configure Everest settings", - } - cmd.AddCommand(settings.NewOIDCCmd(l)) - cmd.AddCommand(settings.NewRBACCmd(l)) - return cmd +var settingsCmd = &cobra.Command{ + Use: "settings [flags]", + Args: cobra.ExactArgs(1), + Long: "Configure Everest settings", + Short: "Configure Everest settings", + Run: func(_ *cobra.Command, _ []string) {}, +} + +func init() { + rootCmd.AddCommand(settingsCmd) + + settingsCmd.AddCommand(settings.GetSettingsOIDCCmd()) + settingsCmd.AddCommand(settings.GetSettingsRBACCmd()) } diff --git a/commands/settings/oidc.go b/commands/settings/oidc.go index a513b21eb..81b3f1f6e 100644 --- a/commands/settings/oidc.go +++ b/commands/settings/oidc.go @@ -13,25 +13,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package settings ... +// Package settings provides the Everest settings CLI commands. package settings import ( "github.com/spf13/cobra" - "go.uber.org/zap" "github.com/percona/everest/commands/settings/oidc" ) -// NewOIDCCmd returns an new OIDC sub-command. -func NewOIDCCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "oidc", - Long: "Manage settings related to OIDC", - Short: "Manage settings related to OIDC", - } +var settingsOIDCCmd = &cobra.Command{ + Use: "oidc [flags]", + Args: cobra.ExactArgs(1), + Long: "Manage settings related to OIDC", + Short: "Manage settings related to OIDC", +} - cmd.AddCommand(oidc.NewConfigureCommand(l)) +func init() { + settingsOIDCCmd.AddCommand(oidc.GetSettingsOIDCConfigureCmd()) +} - return cmd +// GetSettingsOIDCCmd returns the command to manage OIDC settings. +func GetSettingsOIDCCmd() *cobra.Command { + return settingsOIDCCmd } diff --git a/commands/settings/oidc/configure.go b/commands/settings/oidc/configure.go index 12c4bc13e..418f319b6 100644 --- a/commands/settings/oidc/configure.go +++ b/commands/settings/oidc/configure.go @@ -13,66 +13,59 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package oidc ... +// Package oidc provides OIDC settings CLI commands. package oidc import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" + "github.com/percona/everest/pkg/cli" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/oidc" "github.com/percona/everest/pkg/output" ) -// NewConfigureCommand returns the command to configure OIDC. -func NewConfigureCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "configure", - Long: "Configure OIDC settings", - Short: "Configure OIDC settings", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initOIDCViperFlags(cmd) - c, err := parseOIDCConfig() - if err != nil { - l.Error(err) - os.Exit(1) - } - - op, err := oidc.NewOIDC(*c, l) - if err != nil { - l.Error(err) - os.Exit(1) - } - - if err := op.Run(cmd.Context()); err != nil { - output.PrintError(err, l, false) - os.Exit(1) - } - }, +var ( + settingsOIDCConfigureCmd = &cobra.Command{ + Use: "configure [flags]", + Args: cobra.NoArgs, + Long: "Configure OIDC settings", + Short: "Configure OIDC settings", + Example: `everestctl settings oidc configure --issuer-url https://example.com --client-id 123456`, + PreRun: settingsOIDCConfigurePreRun, + Run: settingsOIDCConfigureRun, } + settingsOIDCConfigureCfg = &oidc.Config{} +) - initOIDCFlags(cmd) - - return cmd +func init() { + // local command flags + settingsOIDCConfigureCmd.Flags().StringVar(&settingsOIDCConfigureCfg.IssuerURL, cli.FlagOIDCIssueURL, "", "OIDC issuer url") + settingsOIDCConfigureCmd.Flags().StringVar(&settingsOIDCConfigureCfg.ClientID, cli.FlagOIDCIssueClientID, "", "OIDC application client ID") } -func initOIDCFlags(cmd *cobra.Command) { - cmd.Flags().String("issuer-url", "", "OIDC issuer url") - cmd.Flags().String("client-id", "", "ID of the client OIDC app") +func settingsOIDCConfigurePreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + settingsOIDCConfigureCfg.Pretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + settingsOIDCConfigureCfg.KubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() } -func initOIDCViperFlags(cmd *cobra.Command) { - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec - viper.BindPFlag("issuer-url", cmd.Flags().Lookup("issuer-url")) //nolint:errcheck,gosec - viper.BindPFlag("client-id", cmd.Flags().Lookup("client-id")) //nolint:errcheck,gosec +func settingsOIDCConfigureRun(cmd *cobra.Command, _ []string) { + op, err := oidc.NewOIDC(*settingsOIDCConfigureCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } + + if err := op.Run(cmd.Context()); err != nil { + output.PrintError(err, logger.GetLogger(), settingsOIDCConfigureCfg.Pretty) + os.Exit(1) + } } -func parseOIDCConfig() (*oidc.Config, error) { - c := &oidc.Config{} - err := viper.Unmarshal(c) - return c, err +// GetSettingsOIDCConfigureCmd returns the command to configure OIDC settings. +func GetSettingsOIDCConfigureCmd() *cobra.Command { + return settingsOIDCConfigureCmd } diff --git a/commands/settings/rbac.go b/commands/settings/rbac.go index 10781ce7d..8681576fb 100644 --- a/commands/settings/rbac.go +++ b/commands/settings/rbac.go @@ -1,21 +1,40 @@ -// Package settings ... +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 settings provides the Everest settings CLI commands. package settings import ( "github.com/spf13/cobra" - "go.uber.org/zap" "github.com/percona/everest/commands/settings/rbac" ) -// NewRBACCmd returns an new RBAC sub-command. -func NewRBACCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "rbac", - Long: "Manage RBAC settings", - Short: "Manage RBAC settings", - } - cmd.AddCommand(rbac.NewValidateCommand(l)) - cmd.AddCommand(rbac.NewCanCommand(l)) - return cmd +var settingsRBACCmd = &cobra.Command{ + Use: "rbac [flags]", + Args: cobra.ExactArgs(1), + Long: "Manage RBAC settings", + Short: "Manage RBAC settings", +} + +func init() { + settingsRBACCmd.AddCommand(rbac.GetSettingsRBACValidateCmd()) + settingsRBACCmd.AddCommand(rbac.GetSettingsRBACCanCmd()) +} + +// GetSettingsRBACCmd returns the command to manage RBAC settings. +func GetSettingsRBACCmd() *cobra.Command { + return settingsRBACCmd } diff --git a/commands/settings/rbac/can.go b/commands/settings/rbac/can.go index 3ba726f81..b13b037a5 100644 --- a/commands/settings/rbac/can.go +++ b/commands/settings/rbac/can.go @@ -13,21 +13,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package rbac ... +// Package rbac provides RBAC settings CLI commands. package rbac import ( "errors" "fmt" - "net/url" "os" + "strings" "github.com/spf13/cobra" - "github.com/spf13/viper" "go.uber.org/zap" - "k8s.io/client-go/tools/clientcmd" + "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/rbac" ) @@ -52,68 +52,73 @@ NOTE: The asterisk character (*) holds a special meaning in the unix shell. To prevent misinterpretation, you need to add single quotes around it. ` -// NewCanCommand returns a new command for testing RBAC. -func NewCanCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "can SUBJECT ACTION RESOURCE SUBRESOURCE", +var ( + settingsRBACCanCmd = &cobra.Command{ + Use: "can [flags]", + Args: cobra.ExactArgs(4), Long: `Test RBAC policy.` + "\n" + canCmdExamples, Short: "Test RBAC policy", Example: "everestctl settings rbac can alice read database-clusters all", - Run: func(cmd *cobra.Command, args []string) { - initCanViperFlags(cmd) - - kubeconfigPath := viper.GetString("kubeconfig") - policyFilepath := viper.GetString("policy-file") - if kubeconfigPath == "" && policyFilepath == "" { - l.Error("Either --kubeconfig or --policy-file must be set") - os.Exit(1) - } - - var k *kubernetes.Kubernetes - if kubeconfigPath != "" && policyFilepath == "" { - client, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) || errors.Is(err, clientcmd.ErrEmptyConfig) { - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - os.Exit(1) - } - k = client - } - - if len(args) != 4 { //nolint:mnd - l.Error("invalid number of arguments provided") - if err := cmd.Usage(); err != nil { - panic(err) - } - os.Exit(1) - } - - can, err := rbac.Can(cmd.Context(), policyFilepath, k, args...) - if err != nil { - l.Error(err) - os.Exit(1) - } - - if can { - fmt.Fprintln(os.Stdout, "Yes") - return - } - fmt.Fprintln(os.Stdout, "No") - }, + PreRunE: settingsRBACCanPreRunE, + Run: settingsRBACCanRun, } - initCanFlags(cmd) - return cmd + rbacCanPolicyFilePath string + rbacCanKubeconfigPath string + rbacCanPretty bool +) + +func init() { + // local command flags + settingsRBACCanCmd.Flags().StringVar(&rbacCanPolicyFilePath, cli.FlagRBACPolicyFile, "", "Path to the policy file to use, otherwise use policy from Everest deployment.") +} + +func settingsRBACCanPreRunE(cmd *cobra.Command, args []string) error { //nolint:revive + // validate action + if !rbac.ValidateAction(args[1]) { + return errors.New(fmt.Sprintf("invalid action '%s'. Supported actions: %s", + args[1], strings.Join(rbac.SupportedActions, `,`), + )) + } + + // Copy global flags to config + rbacCanPretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + rbacCanKubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() + return nil } -func initCanFlags(cmd *cobra.Command) { - cmd.Flags().String("policy-file", "", "Path to the policy file to use") +func settingsRBACCanRun(cmd *cobra.Command, args []string) { + var k *kubernetes.Kubernetes + if rbacCanPolicyFilePath == "" { + // check over policy in Everest deployment (ConfigMap). + var l *zap.SugaredLogger + if rbacCanPretty { + l = zap.NewNop().Sugar() + } else { + l = logger.GetLogger().With("component", "rbac") + } + + client, err := kubernetes.New(rbacCanKubeconfigPath, l) + if err != nil { + l.Error(err) + os.Exit(1) + } + k = client + } + + can, err := rbac.Can(cmd.Context(), rbacCanPolicyFilePath, k, args...) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } + + if can { + _, _ = fmt.Fprintln(os.Stdout, "Yes") + return + } + _, _ = fmt.Fprintln(os.Stdout, "No") } -func initCanViperFlags(cmd *cobra.Command) { - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec - viper.BindPFlag("policy-file", cmd.Flags().Lookup("policy-file")) //nolint:errcheck,gosec +// GetSettingsRBACCanCmd returns the command to test RBAC policy. +func GetSettingsRBACCanCmd() *cobra.Command { + return settingsRBACCanCmd } diff --git a/commands/settings/rbac/validate.go b/commands/settings/rbac/validate.go index 8f1dabda8..ed2f4399d 100644 --- a/commands/settings/rbac/validate.go +++ b/commands/settings/rbac/validate.go @@ -13,77 +13,80 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package rbac ... +// Package rbac provides RBAC settings CLI commands. package rbac import ( - "errors" "fmt" - "net/url" "os" "strings" "github.com/spf13/cobra" - "github.com/spf13/viper" "go.uber.org/zap" - "k8s.io/client-go/tools/clientcmd" + "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/output" "github.com/percona/everest/pkg/rbac" ) -// NewValidateCommand returns a new command for validating RBAC. -func NewValidateCommand(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "validate", - Long: "Validate RBAC settings", - Short: "Validate RBAC settings", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initValidateViperFlags(cmd) +var ( + settingsRBACValidateCmd = &cobra.Command{ + Use: "validate [flags]", + Long: "Validate RBAC settings", + Short: "Validate RBAC settings", + Example: "everestctl settings rbac validate --policy-file ", + PreRun: settingsRBACValidatePreRun, + Run: settingsRBACValidateRun, + } + rbacValidatePolicyFilePath string + rbacValidateKubeconfigPath string + rbacValidatePretty bool +) - kubeconfigPath := viper.GetString("kubeconfig") - policyFilepath := viper.GetString("policy-file") - if kubeconfigPath == "" && policyFilepath == "" { - l.Error("Either --kubeconfig or --policy-file must be set") - os.Exit(1) - } +func init() { + // local command flags + settingsRBACValidateCmd.Flags().StringVar(&rbacValidatePolicyFilePath, cli.FlagRBACPolicyFile, "", "Path to the policy file to use, otherwise use policy from Everest deployment.") +} - var k *kubernetes.Kubernetes - if kubeconfigPath != "" && policyFilepath == "" { - client, err := kubernetes.New(kubeconfigPath, l) - if err != nil { - var u *url.Error - if errors.As(err, &u) || errors.Is(err, clientcmd.ErrEmptyConfig) { - l.Error("Could not connect to Kubernetes. " + - "Make sure Kubernetes is running and is accessible from this computer/server.") - } - os.Exit(1) - } - k = client - } +func settingsRBACValidatePreRun(cmd *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + rbacValidatePretty = !(cmd.Flag(cli.FlagVerbose).Changed || cmd.Flag(cli.FlagJSON).Changed) + rbacValidateKubeconfigPath = cmd.Flag(cli.FlagKubeconfig).Value.String() +} - err := rbac.ValidatePolicy(cmd.Context(), k, policyFilepath) - if err != nil { - fmt.Fprint(os.Stdout, output.Failure("Invalid")) - msg := err.Error() - msg = strings.Join(strings.Split(msg, "\n"), " - ") - fmt.Fprintln(os.Stdout, msg) - os.Exit(1) - } - fmt.Fprintln(os.Stdout, output.Success("Valid")) - }, +func settingsRBACValidateRun(cmd *cobra.Command, _ []string) { + var k *kubernetes.Kubernetes + if rbacValidatePolicyFilePath == "" { + // check over policy in Everest deployment (ConfigMap). + var l *zap.SugaredLogger + if rbacValidatePretty { + l = zap.NewNop().Sugar() + } else { + l = logger.GetLogger().With("component", "rbac") + } + + client, err := kubernetes.New(rbacValidateKubeconfigPath, l) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } + k = client } - initValidateFlags(cmd) - return cmd -} -func initValidateFlags(cmd *cobra.Command) { - cmd.Flags().String("policy-file", "", "Path to the policy file to use") + err := rbac.ValidatePolicy(cmd.Context(), k, rbacValidatePolicyFilePath) + if err != nil { + _, _ = fmt.Fprint(os.Stdout, output.Failure("Invalid")) + msg := err.Error() + msg = strings.Join(strings.Split(msg, "\n"), " - ") + _, _ = fmt.Fprintln(os.Stdout, msg) + os.Exit(1) + } + _, _ = fmt.Fprintln(os.Stdout, output.Success("Valid")) } -func initValidateViperFlags(cmd *cobra.Command) { - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec - viper.BindPFlag("policy-file", cmd.Flags().Lookup("policy-file")) //nolint:errcheck,gosec +// GetSettingsRBACValidateCmd returns the command to validate RBAC policies. +func GetSettingsRBACValidateCmd() *cobra.Command { + return settingsRBACValidateCmd } diff --git a/commands/uninstall.go b/commands/uninstall.go index a2f3b12b7..d6ef4a459 100644 --- a/commands/uninstall.go +++ b/commands/uninstall.go @@ -17,78 +17,52 @@ package commands import ( - "fmt" "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" + "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/cli/uninstall" - "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/output" ) -// newUninstallCmd returns a new uninstall command. -func newUninstallCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "uninstall", - Long: "Uninstall Percona Everest", - Short: "Uninstall Percona Everest", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initUninstallViperFlags(cmd) - c, err := parseClusterConfig() - if err != nil { - os.Exit(1) - } - - enableLogging := viper.GetBool("verbose") || viper.GetBool("json") - c.Pretty = !enableLogging - - op, err := uninstall.NewUninstall(*c, l) - if err != nil { - l.Error(err) - os.Exit(1) - } - - if err := op.Run(cmd.Context()); err != nil { - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - }, +var ( + uninstallCmd = &cobra.Command{ + Use: "uninstall [flags]", + Args: cobra.NoArgs, + Long: "Uninstall Percona Everest", + Short: "Uninstall Percona Everest", + PreRun: uninstallPreRun, + Run: uninstallRun, } + uninstallCfg = &uninstall.Config{} +) - initUninstallFlags(cmd) +func init() { + rootCmd.AddCommand(uninstallCmd) - return cmd + // local command flags + uninstallCmd.Flags().BoolVarP(&uninstallCfg.AssumeYes, "assume-yes", "y", false, "Assume yes to all questions") + uninstallCmd.Flags().BoolVarP(&uninstallCfg.Force, "force", "f", false, "Force removal in case there are database clusters running") + uninstallCmd.Flags().BoolVar(&uninstallCfg.SkipEnvDetection, cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") } -func initUninstallFlags(cmd *cobra.Command) { - cmd.Flags().BoolP("assume-yes", "y", false, "Assume yes to all questions") - cmd.Flags().BoolP("force", "f", false, "Force removal in case there are database clusters running") - - cmd.Flags().Bool(uninstall.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") - cmd.Flags().String(uninstall.FlagCatalogNamespace, kubernetes.OLMNamespace, - fmt.Sprintf("Namespace where Everest OLM catalog is installed. Implies --%s", uninstall.FlagSkipEnvDetection), - ) - cmd.Flags().Bool(uninstall.FlagSkipOLM, false, fmt.Sprintf("Skip OLM uninstallation. Implies --%s", uninstall.FlagSkipEnvDetection)) +func uninstallPreRun(_ *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + uninstallCfg.Pretty = rootCmdFlags.Pretty + uninstallCfg.KubeconfigPath = rootCmdFlags.KubeconfigPath } -func initUninstallViperFlags(cmd *cobra.Command) { - viper.BindEnv("kubeconfig") //nolint:errcheck,gosec - viper.BindPFlag("kubeconfig", cmd.Flags().Lookup("kubeconfig")) //nolint:errcheck,gosec - viper.BindPFlag("assume-yes", cmd.Flags().Lookup("assume-yes")) //nolint:errcheck,gosec - viper.BindPFlag("force", cmd.Flags().Lookup("force")) //nolint:errcheck,gosec - viper.BindPFlag("verbose", cmd.Flags().Lookup("verbose")) //nolint:errcheck,gosec - viper.BindPFlag("json", cmd.Flags().Lookup("json")) //nolint:errcheck,gosec - - viper.BindPFlag(uninstall.FlagSkipEnvDetection, cmd.Flags().Lookup(uninstall.FlagSkipEnvDetection)) //nolint:errcheck,gosec - viper.BindPFlag(uninstall.FlagSkipOLM, cmd.Flags().Lookup(uninstall.FlagSkipOLM)) //nolint:errcheck,gosec - viper.BindPFlag(uninstall.FlagCatalogNamespace, cmd.Flags().Lookup(uninstall.FlagCatalogNamespace)) //nolint:errcheck,gosec -} +func uninstallRun(cmd *cobra.Command, _ []string) { //nolint:revive + op, err := uninstall.NewUninstall(*uninstallCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } -func parseClusterConfig() (*uninstall.Config, error) { - c := &uninstall.Config{} - err := viper.Unmarshal(c) - return c, err + if err := op.Run(cmd.Context()); err != nil { + output.PrintError(err, logger.GetLogger(), uninstallCfg.Pretty) + os.Exit(1) + } } diff --git a/commands/upgrade.go b/commands/upgrade.go index 04070b10b..531e17536 100644 --- a/commands/upgrade.go +++ b/commands/upgrade.go @@ -20,96 +20,63 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/cli/helm" "github.com/percona/everest/pkg/cli/upgrade" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/output" ) -func newUpgradeCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "upgrade", - // The command expects no arguments. So to prevent users from misspelling and confusion - // in cases with unexpected spaces like - // ./everestctl upgrade --namespaces=aaa, a - // it will return - // Error: unknown command "a" for "everestctl upgrade" - Args: cobra.NoArgs, - Long: "Upgrade Percona Everest using Helm", - Short: "Upgrade Percona Everest using Helm", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - initUpgradeViperFlags(cmd) - - c, err := parseUpgradeConfig() - if err != nil { - os.Exit(1) - } - c.CLIOptions.BindViperFlags() - - enableLogging := viper.GetBool("verbose") || viper.GetBool("json") - c.Pretty = !enableLogging - - op, err := upgrade.NewUpgrade(c, l) - if err != nil { - l.Error(err) - os.Exit(1) - } - - if err := op.Run(cmd.Context()); err != nil { - output.PrintError(err, l, !enableLogging) - os.Exit(1) - } - }, +var ( + upgradeCmd = &cobra.Command{ + Use: "upgrade [flags]", + Args: cobra.NoArgs, + Long: "Upgrade Percona Everest using Helm", + Short: "Upgrade Percona Everest using Helm", + PreRun: upgradePreRun, + Run: upgradeRun, } - initUpgradeFlags(cmd) + upgradeCfg = &upgrade.Config{} +) - return cmd +func init() { + rootCmd.AddCommand(upgradeCmd) + + // local command flags + upgradeCmd.Flags().StringVar(&upgradeCfg.VersionMetadataURL, cli.FlagVersionMetadataURL, "https://check.percona.com", "URL to retrieve version metadata information from") + upgradeCmd.Flags().BoolVar(&upgradeCfg.SkipEnvDetection, cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") + upgradeCmd.Flags().BoolVar(&upgradeCfg.DryRun, cli.FlagUpgradeDryRun, false, "If set, only executes the pre-upgrade checks") + upgradeCmd.Flags().BoolVar(&upgradeCfg.InCluster, cli.FlagUpgradeInCluster, false, "If set, uses the in-cluster Kubernetes client configuration") + _ = upgradeCmd.Flags().MarkHidden(cli.FlagUpgradeInCluster) + + // --helm.* flags + upgradeCmd.Flags().StringVar(&upgradeCfg.ChartDir, helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") + _ = upgradeCmd.Flags().MarkHidden(helm.FlagChartDir) + upgradeCmd.Flags().StringVar(&upgradeCfg.RepoURL, helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") + upgradeCmd.Flags().StringSliceVar(&upgradeCfg.Values.Values, helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") + upgradeCmd.Flags().StringSliceVarP(&upgradeCfg.Values.ValueFiles, helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") + upgradeCmd.Flags().BoolVar(&upgradeCfg.ResetThenReuseValues, helm.FlagHelmReuseValues, false, "Reuse the last release's values and merge in any overrides from the command line via --helm.set and -f") + upgradeCmd.Flags().BoolVar(&upgradeCfg.ResetValues, helm.FlagHelmResetValues, false, "Reset the values to the ones built into the chart") + upgradeCmd.Flags().BoolVar(&upgradeCfg.ResetThenReuseValues, helm.FlagHelmResetThenReuseValues, false, "Reset the values to the ones built into the chart, apply the last release's values and merge in any overrides from the command line via --set and -f.") } -func initUpgradeFlags(cmd *cobra.Command) { - cmd.Flags().String(cli.FlagVersionMetadataURL, "https://check.percona.com", "URL to retrieve version metadata information from") - cmd.Flags().Bool(cli.FlagSkipEnvDetection, false, "Skip detecting Kubernetes environment where Everest is installed") - - cmd.Flags().String(helm.FlagChartDir, "", "Path to the chart directory. If not set, the chart will be downloaded from the repository") - cmd.Flags().MarkHidden(helm.FlagChartDir) //nolint:errcheck,gosec - cmd.Flags().String(helm.FlagRepository, helm.DefaultHelmRepoURL, "Helm chart repository to download the Everest charts from") - cmd.Flags().StringSlice(helm.FlagHelmSet, []string{}, "Set helm values on the command line (can specify multiple values with commas: key1=val1,key2=val2)") - cmd.Flags().StringSliceP(helm.FlagHelmValues, "f", []string{}, "Specify values in a YAML file or a URL (can specify multiple)") - cmd.Flags().Bool(helm.FlagHelmReuseValues, false, "Reuse the last release's values and merge in any overrides from the command line via --helm.set and -f") - cmd.Flags().Bool(helm.FlagHelmResetValues, false, "Reset the values to the ones built into the chart") - cmd.Flags().Bool(helm.FlagHelmResetThenReuseValues, false, "Reset the values to the ones built into the chart, apply the last release's values and merge in any overrides from the command line via --set and -f.") - - cmd.Flags().BoolP("logs", "l", false, "If set, logs are printed during the upgrade process") - cmd.Flags().Bool("dry-run", false, "If set, only executes the pre-upgrade checks") - cmd.Flags().Bool("in-cluster", false, "If set, uses the in-cluster Kubernetes client configuration") +func upgradePreRun(_ *cobra.Command, _ []string) { //nolint:revive + // Copy global flags to config + upgradeCfg.Pretty = rootCmdFlags.Pretty + upgradeCfg.KubeconfigPath = rootCmdFlags.KubeconfigPath } -func initUpgradeViperFlags(cmd *cobra.Command) { - viper.BindEnv(cli.FlagKubeconfig) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagKubeconfig, cmd.Flags().Lookup(cli.FlagKubeconfig)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagVersionMetadataURL, cmd.Flags().Lookup(cli.FlagVersionMetadataURL)) //nolint:errcheck,gosec - viper.BindPFlag(cli.FlagVerbose, cmd.Flags().Lookup(cli.FlagVerbose)) //nolint:errcheck,gosec - viper.BindPFlag("json", cmd.Flags().Lookup("json")) //nolint:errcheck,gosec - viper.BindPFlag("dry-run", cmd.Flags().Lookup("dry-run")) //nolint:errcheck,gosec - viper.BindPFlag("in-cluster", cmd.Flags().Lookup("in-cluster")) //nolint:errcheck,gosec - - viper.BindPFlag(upgrade.FlagSkipEnvDetection, cmd.Flags().Lookup(upgrade.FlagSkipEnvDetection)) //nolint:errcheck,gosec - - viper.BindPFlag(helm.FlagChartDir, cmd.Flags().Lookup(helm.FlagChartDir)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagRepository, cmd.Flags().Lookup(helm.FlagRepository)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmSet, cmd.Flags().Lookup(helm.FlagHelmSet)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmValues, cmd.Flags().Lookup(helm.FlagHelmValues)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmReuseValues, cmd.Flags().Lookup(helm.FlagHelmReuseValues)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmResetValues, cmd.Flags().Lookup(helm.FlagHelmResetValues)) //nolint:errcheck,gosec - viper.BindPFlag(helm.FlagHelmResetThenReuseValues, cmd.Flags().Lookup(helm.FlagHelmResetThenReuseValues)) //nolint:errcheck,gosec -} +func upgradeRun(cmd *cobra.Command, _ []string) { //nolint:revive + op, err := upgrade.NewUpgrade(upgradeCfg, logger.GetLogger()) + if err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } -func parseUpgradeConfig() (*upgrade.Config, error) { - c := &upgrade.Config{} - err := viper.Unmarshal(c) - return c, err + if err := op.Run(cmd.Context()); err != nil { + output.PrintError(err, logger.GetLogger(), upgradeCfg.Pretty) + os.Exit(1) + } } diff --git a/commands/version.go b/commands/version.go index f486d2476..e4a23b9f0 100644 --- a/commands/version.go +++ b/commands/version.go @@ -1,3 +1,18 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 commands import ( @@ -5,75 +20,63 @@ import ( "os" "github.com/spf13/cobra" - "go.uber.org/zap" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/percona/everest/pkg/cli/utils" + "github.com/percona/everest/pkg/logger" "github.com/percona/everest/pkg/version" ) -func newVersionCmd(l *zap.SugaredLogger) *cobra.Command { - cmd := &cobra.Command{ - Use: "version", - Long: "Print version info", - Short: "Print version info", - Run: func(cmd *cobra.Command, args []string) { //nolint:revive - outputJSON, err := cmd.Flags().GetBool("json") - if err != nil { - l.Errorf("could not parse json global flag. Error: %s", err) - os.Exit(1) - return - } - kubeConfigPath, err := cmd.Flags().GetString("kubeconfig") - if err != nil { - l.Errorf("could not parse kubeconfig global flag. Error: %s", err) - os.Exit(1) - return - } +var ( + versionCmd = &cobra.Command{ + Use: "version [flags]", + Args: cobra.NoArgs, + Long: "Print Everest components version info", + Short: "Print Everest components version info", + RunE: versionRunE, + } - clientOnly, err := cmd.Flags().GetBool("client-only") - if err != nil { - l.Errorf("could not parse client-only flag. Error: %s", err) - os.Exit(1) - return - } + // Command flag values + clientOnlyFlag bool // Means only info about the client shall be printed +) - v := version.Info{ - ProjectName: version.ProjectName, - Version: version.Version, - FullCommit: version.FullCommit, - } +func init() { + rootCmd.AddCommand(versionCmd) - if !clientOnly { - var err error - k, err := utils.NewKubeclient(l, kubeConfigPath) - if err != nil { - os.Exit(1) - return - } - ev, err := version.EverestVersionFromDeployment(cmd.Context(), k) - if client.IgnoreNotFound(err) != nil { - l.Error(err) - os.Exit(1) - } - sv := "[NOT INSTALLED]" - if ev != nil { - sv = fmt.Sprintf("v%s", ev.String()) - } - v.ServerVersion = &sv - } + // local command flags + versionCmd.Flags().BoolVar(&clientOnlyFlag, "client-only", false, "Print client version only") +} - if !outputJSON { - fmt.Fprintln(os.Stdout, v) - return - } - fmt.Fprintln(os.Stdout, v.JSONString()) - }, +func versionRunE(cmd *cobra.Command, _ []string) error { //nolint:revive + v := version.Info{ + ProjectName: version.ProjectName, + Version: version.Version, + FullCommit: version.FullCommit, } - initVersionFlags(cmd) - return cmd -} + cmdLogger := logger.GetLogger().With("component", "version") -func initVersionFlags(cmd *cobra.Command) { - cmd.Flags().Bool("client-only", false, "Print client version only") + if !clientOnlyFlag { + k, err := utils.NewKubeclient(cmdLogger, rootCmdFlags.KubeconfigPath) + if err != nil { + return err + } + + ev, err := version.EverestVersionFromDeployment(cmd.Context(), k) + if client.IgnoreNotFound(err) != nil { + cmdLogger.Error(err) + return err + } + sv := "[NOT INSTALLED]" + if ev != nil { + sv = fmt.Sprintf("v%s", ev.String()) + } + v.ServerVersion = &sv + } + + if rootCmdFlags.JSON { + _, _ = fmt.Fprintln(os.Stdout, v.JSONString()) + return nil + } + _, _ = fmt.Fprintln(os.Stdout, v) + return nil } diff --git a/go.mod b/go.mod index 40b2c94cc..10be7e2db 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,6 @@ require ( github.com/percona/percona-helm-charts/charts/everest v0.0.0-20250123104614-235a3a784a84 github.com/rodaine/table v1.3.0 github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.31.0 @@ -61,6 +60,11 @@ require ( sigs.k8s.io/yaml v1.4.0 ) +require ( + k8s.io/apiserver v0.32.0 // indirect + k8s.io/component-base v0.32.0 // indirect +) + require ( cel.dev/expr v0.18.0 // indirect dario.cat/mergo v1.0.1 // indirect @@ -104,7 +108,6 @@ require ( github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flosch/pongo2/v6 v6.0.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.5.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect @@ -136,7 +139,6 @@ require ( github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.3.1 // indirect @@ -157,7 +159,6 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -179,7 +180,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/operator-framework/operator-registry v1.35.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/percona/percona-backup-mongodb v1.8.1-0.20241002124601-957ac501f939 // indirect github.com/percona/percona-postgresql-operator v0.0.0-20241007204305-35d61aa5aebd // indirect github.com/percona/percona-server-mongodb-operator v1.18.0 // indirect @@ -196,18 +196,13 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/rubenv/sql-migrate v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.6.0 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect @@ -233,9 +228,6 @@ require ( google.golang.org/grpc v1.67.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - k8s.io/apiserver v0.32.0 // indirect - k8s.io/component-base v0.32.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect diff --git a/go.sum b/go.sum index d6f34836c..548cb1ef0 100644 --- a/go.sum +++ b/go.sum @@ -1937,8 +1937,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= -github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= @@ -2056,8 +2054,6 @@ github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WV github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -2220,8 +2216,6 @@ github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/percona/everest-operator v0.6.0-dev1.0.20250113063626-b38e7d1b3932 h1:6rhZkr0rcA6jNf+/X4yNC1F0HiJUZRygbSgOx8krUDQ= github.com/percona/everest-operator v0.6.0-dev1.0.20250113063626-b38e7d1b3932/go.mod h1:76ol+aF1CAkDey7kNkqmgtcjdTzJdEMFegfUiGS3q7M= github.com/percona/percona-backup-mongodb v1.8.1-0.20241002124601-957ac501f939 h1:OggdqSzqe9pO3A4GaRlrLwZXS3zEQ84O4+7Jm9cg74s= @@ -2342,10 +2336,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= @@ -2364,8 +2354,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -2373,8 +2361,6 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= @@ -2390,8 +2376,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= @@ -2418,8 +2402,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -3583,8 +3565,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/pkg/accounts/cli/accounts.go b/pkg/accounts/cli/accounts.go index 557b92499..24423b7dd 100644 --- a/pkg/accounts/cli/accounts.go +++ b/pkg/accounts/cli/accounts.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "os" "regexp" "strings" @@ -28,31 +29,58 @@ import ( "go.uber.org/zap" "github.com/percona/everest/pkg/accounts" + cliutils "github.com/percona/everest/pkg/cli/utils" + "github.com/percona/everest/pkg/common" + "github.com/percona/everest/pkg/kubernetes" + "github.com/percona/everest/pkg/output" ) const ( minPasswordLength = 6 ) -// CLI provides functionality for managing user accounts via the CLI. -type CLI struct { +// Accounts provides functionality for managing user accounts via the Accounts. +type Accounts struct { accountManager accounts.Interface l *zap.SugaredLogger + config Config + kubeClient *kubernetes.Kubernetes } -// New creates a new CLI for running accounts commands. -func New(l *zap.SugaredLogger) *CLI { - return &CLI{ - l: l.With("component", "accounts"), +// Config holds the configuration for the accounts subcommands. +type Config struct { + // KubeconfigPath is a path to a kubeconfig + KubeconfigPath string + // If set, we will print the pretty output. + Pretty bool +} + +// NewAccounts creates a new Accounts for running accounts commands. +func NewAccounts(c Config, l *zap.SugaredLogger) (*Accounts, error) { + cli := &Accounts{ + l: l.With("component", "accounts"), + config: c, + } + if c.Pretty { + cli.l = zap.NewNop().Sugar() + } + + k, err := cliutils.NewKubeclient(cli.l, c.KubeconfigPath) + if err != nil { + return nil, err } + cli.kubeClient = k + cli.accountManager = k.Accounts() + + return cli, nil } -// WithAccountManager sets the account manager for the CLI. -func (c *CLI) WithAccountManager(m accounts.Interface) { +// WithAccountManager sets the account manager for the Accounts. +func (c *Accounts) WithAccountManager(m accounts.Interface) { c.accountManager = m } -func (c *CLI) runCredentialsWizard(username, password *string) error { +func (c *Accounts) runCredentialsWizard(username, password *string) error { if *username == "" { pUsername := survey.Input{ Message: "Enter username", @@ -72,22 +100,30 @@ func (c *CLI) runCredentialsWizard(username, password *string) error { return nil } +// SetPasswordOptions holds options for setting a new password for user accounts. +type SetPasswordOptions struct { + // Username is the username for the account. + Username string + // NewPassword is a new password for the account. + NewPassword string +} + // SetPassword sets the password for an existing account. -func (c *CLI) SetPassword(ctx context.Context, username, password string) error { - if username == "" { +func (c *Accounts) SetPassword(ctx context.Context, opts SetPasswordOptions) error { + if opts.Username == "" { pUsername := survey.Input{ Message: "Enter username", } - if err := survey.AskOne(&pUsername, &username); err != nil { + if err := survey.AskOne(&pUsername, &opts.Username); err != nil { return err } } - if username == "" { + if opts.Username == "" { return errors.New("username is required") } - if password == "" { + if opts.NewPassword == "" { resp := struct { Password string ConfPassword string @@ -110,44 +146,61 @@ func (c *CLI) SetPassword(ctx context.Context, username, password string) error if resp.Password != resp.ConfPassword { return errors.New("passwords do not match") } - password = resp.Password + opts.NewPassword = resp.Password } - if ok, msg := validateCredentials(username, password); !ok { + c.l.Infof("Setting a new password for user '%s'", opts.Username) + if ok, msg := validateCredentials(opts.Username, opts.NewPassword); !ok { c.l.Error(msg) return errors.New("invalid credentials") } - if err := c.accountManager.SetPassword(ctx, username, password, true); err != nil { + if err := c.accountManager.SetPassword(ctx, opts.Username, opts.NewPassword, true); err != nil { return err } - c.l.Infof("Password updated for user '%s'", username) + + c.l.Infof("Password for user '%s' has been set succesfully", opts.Username) + if c.config.Pretty { + _, _ = fmt.Fprintln(os.Stdout, output.Success("Password for user '%s' has been set successfully", opts.Username)) + } + return nil } +// CreateOptions holds options for creating a new user accounts. +type CreateOptions struct { + // Username is the username for the account. + Username string + // Password is the password for the account. + Password string +} + // Create a new user account. -func (c *CLI) Create(ctx context.Context, username, password string) error { - if err := c.runCredentialsWizard(&username, &password); err != nil { +func (c *Accounts) Create(ctx context.Context, opts CreateOptions) error { + if err := c.runCredentialsWizard(&opts.Username, &opts.Password); err != nil { return err } - if username == "" { - return errors.New("username is required") - } - if ok, msg := validateCredentials(username, password); !ok { + if ok, msg := validateCredentials(opts.Username, opts.Password); !ok { c.l.Error(msg) return errors.New("invalid credentials") } - if err := c.accountManager.Create(ctx, username, password); err != nil { + c.l.Infof("Creating user '%s'", opts.Username) + if err := c.accountManager.Create(ctx, opts.Username, opts.Password); err != nil { return err } - c.l.Infof("User '%s' has been created", username) + + c.l.Infof("User '%s' has been created succesfully", opts.Username) + if c.config.Pretty { + _, _ = fmt.Fprintln(os.Stdout, output.Success("User '%s' has been created successfully", opts.Username)) + } + return nil } // Delete an existing user account. -func (c *CLI) Delete(ctx context.Context, username string) error { +func (c *Accounts) Delete(ctx context.Context, username string) error { if username == "" { if err := survey.AskOne(&survey.Input{ Message: "Enter username", @@ -159,28 +212,39 @@ func (c *CLI) Delete(ctx context.Context, username string) error { if username == "" { return errors.New("username is required") } - return c.accountManager.Delete(ctx, username) + + c.l.Infof("Deleting user '%s'", username) + if err := c.accountManager.Delete(ctx, username); err != nil { + return err + } + + c.l.Infof("User '%s' has been deleted succesfully", username) + if c.config.Pretty { + _, _ = fmt.Fprintln(os.Stdout, output.Success("User '%s' has been deleted successfully", username)) + } + + return nil } // ListOptions holds options for listing user accounts. type ListOptions struct { - NoHeaders bool `mapstructure:"no-headers"` - Columns []string `mapstructure:"columns"` + NoHeaders bool + Columns []string } const ( - columnUser = "user" - columnCapabilities = "capabilities" - columnEnabled = "enabled" + // ColumnUser is the column name for the user. + ColumnUser = "user" + // ColumnCapabilities is the column name for the capabilities. + ColumnCapabilities = "capabilities" + // ColumnEnabled is the column name for the enabled status. + ColumnEnabled = "enabled" ) // List all user accounts in the system. -func (c *CLI) List(ctx context.Context, opts *ListOptions) error { - if opts == nil { - opts = &ListOptions{} - } +func (c *Accounts) List(ctx context.Context, opts ListOptions) error { // Prepare table headings. - headings := []interface{}{columnUser, columnCapabilities, columnEnabled} + headings := []interface{}{ColumnUser, ColumnCapabilities, ColumnEnabled} if len(opts.Columns) > 0 { headings = []interface{}{} for _, col := range opts.Columns { @@ -206,11 +270,11 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { row := []any{} for _, heading := range headings { switch heading { - case "user": + case ColumnUser: row = append(row, user) - case "capabilities": + case ColumnCapabilities: row = append(row, account.Capabilities) - case "enabled": + case ColumnEnabled: row = append(row, account.Enabled) } } @@ -223,6 +287,23 @@ func (c *CLI) List(ctx context.Context, opts *ListOptions) error { return nil } +// GetInitAdminPassword returns the initial admin password. +func (c *Accounts) GetInitAdminPassword(ctx context.Context) (string, error) { + secure, err := c.accountManager.IsSecure(ctx, common.EverestAdminUser) + if err != nil { + return "", err + } + if secure { + return "", errors.New("cannot retrieve admin password after it has been updated") + } + + admin, err := c.accountManager.Get(ctx, common.EverestAdminUser) + if err != nil { + return "", err + } + return admin.PasswordHash, nil +} + func validateCredentials(username, password string) (bool, string) { if !validateUsername(username) { return false, @@ -235,6 +316,21 @@ func validateCredentials(username, password string) (bool, string) { return true, "" } +// CreateRSAKeyPair creates a new RSA key pair for user authentication. New RSA key pair is stored in the Kubernetes secret. +func (c *Accounts) CreateRSAKeyPair(ctx context.Context) error { + c.l.Info("Creating/Updating JWT keys and restarting Everest.") + if err := c.kubeClient.CreateRSAKeyPair(ctx); err != nil { + c.l.Error(err) + os.Exit(1) + } + + c.l.Info("JWT keys have been created/updated successfully") + if c.config.Pretty { + _, _ = fmt.Fprintln(os.Stdout, output.Success("JWT keys have been created/updated successfully")) + } + return nil +} + func validateUsername(username string) bool { // Regular expression to validate username. // [a-zA-Z0-9_] - Allowed characters (letters, digits, underscore) diff --git a/pkg/cli/flags.go b/pkg/cli/flags.go index 63d3b3972..c7cdf4481 100644 --- a/pkg/cli/flags.go +++ b/pkg/cli/flags.go @@ -17,10 +17,17 @@ package cli const ( + // global flags + // FlagKubeconfig is the name of the kubeconfig flag. FlagKubeconfig = "kubeconfig" // FlagVerbose is the name of the verbose flag. FlagVerbose = "verbose" + // FlagJSON is the name of the json flag. + FlagJSON = "json" + + // `install` flags + // FlagOperatorPostgresql represents the pg operator flag. FlagOperatorPostgresql = "operator.postgresql" // FlagOperatorXtraDBCluster represents the pxc operator flag. @@ -39,8 +46,40 @@ const ( FlagSkipEnvDetection = "skip-env-detection" // FlagDisableTelemetry disables telemetry. FlagDisableTelemetry = "disable-telemetry" - // FlagTakeNamespaceOwnership is the name of the take-ownership flag. - FlagTakeNamespaceOwnership = "take-ownership" // FlagInstallSkipDBNamespace is the name of the skip-db-namespace flag. FlagInstallSkipDBNamespace = "skip-db-namespace" + + // `namespaces` flags + + // FlagTakeNamespaceOwnership is the name of the take-ownership flag. + FlagTakeNamespaceOwnership = "take-ownership" + // FlagKeepNamespace is the name of the keep-namespace flag. + FlagKeepNamespace = "keep-namespace" + // FlagNamespaceForce is the name of the force flag. + FlagNamespaceForce = "force" + + // `upgrade` flags + + // FlagUpgradeDryRun is the name of the dry-run flag. + FlagUpgradeDryRun = "dry-run" + // FlagUpgradeInCluster is the name of the in-cluster flag. + FlagUpgradeInCluster = "in-cluster" + + // `accounts` flags + + // FlagAccountsUsername is the name of the username flag. + FlagAccountsUsername = "username" + // FlagAccountsCreatePassword is the name of the password flag. + FlagAccountsCreatePassword = "password" + // FlagAccountsNewPassword is the name of the new-password flag. + FlagAccountsNewPassword = "new-password" + + // settings flags + + // FlagOIDCIssueURL is the name of the issuer-url flag. + FlagOIDCIssueURL = "issuer-url" + // FlagOIDCIssueClientID is the name of the client-id flag. + FlagOIDCIssueClientID = "client-id" + // FlagRBACPolicyFile is the name of the policy-file flag. + FlagRBACPolicyFile = "policy-file" ) diff --git a/pkg/cli/helm/installer.go b/pkg/cli/helm/installer.go index eea8f5dcc..5fade2def 100644 --- a/pkg/cli/helm/installer.go +++ b/pkg/cli/helm/installer.go @@ -26,7 +26,6 @@ import ( "path" "strings" - "github.com/spf13/viper" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" @@ -53,18 +52,6 @@ type CLIOptions struct { ResetThenReuseValues bool } -// BindViperFlags parses the CLI flags from Viper and binds them to the CLI options. -func (o *CLIOptions) BindViperFlags() { - o.Values.Values = viper.GetStringSlice(FlagHelmSet) - o.Values.ValueFiles = viper.GetStringSlice(FlagHelmValues) - o.ChartDir = viper.GetString(FlagChartDir) - o.RepoURL = viper.GetString(FlagRepository) - o.RepoURL = viper.GetString(FlagRepository) - o.ReuseValues = viper.GetBool(FlagHelmReuseValues) - o.ResetValues = viper.GetBool(FlagHelmResetValues) - o.ResetThenReuseValues = viper.GetBool(FlagHelmResetThenReuseValues) -} - // Everest Helm chart names. const ( EverestChartName = "everest" diff --git a/pkg/cli/install/install.go b/pkg/cli/install/install.go index 4b6a3756a..ab71fdea6 100644 --- a/pkg/cli/install/install.go +++ b/pkg/cli/install/install.go @@ -28,12 +28,10 @@ import ( versionpb "github.com/Percona-Lab/percona-version-service/versionpb" "github.com/fatih/color" goversion "github.com/hashicorp/go-version" - "github.com/spf13/cobra" "go.uber.org/zap" k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/percona/everest/pkg/cli" "github.com/percona/everest/pkg/cli/helm" helmutils "github.com/percona/everest/pkg/cli/helm/utils" "github.com/percona/everest/pkg/cli/namespaces" @@ -61,7 +59,6 @@ type Install struct { l *zap.SugaredLogger config Config - cmd *cobra.Command kubeClient *kubernetes.Kubernetes versionService versionservice.Interface @@ -74,29 +71,32 @@ type Install struct { // Config holds the configuration for the install command. type Config struct { // KubeconfigPath is a path to a kubeconfig - KubeconfigPath string `mapstructure:"kubeconfig"` + KubeconfigPath string // VersionMetadataURL stores hostname to retrieve version metadata information from. - VersionMetadataURL string `mapstructure:"version-metadata-url"` + VersionMetadataURL string // Version defines the version to be installed. If empty, the latest version is installed. - Version string `mapstructure:"version"` + Version string // DisableTelemetry disables telemetry. - DisableTelemetry bool `mapstructure:"disable-telemetry"` + DisableTelemetry bool // SkipEnvDetection skips detecting the Kubernetes environment. - SkipEnvDetection bool `mapstructure:"skip-env-detection"` + SkipEnvDetection bool // If set, we will print the pretty output. Pretty bool // SkipDBNamespace is set if the installation should skip provisioning database. - SkipDBNamespace bool `mapstructure:"skip-db-namespace"` + SkipDBNamespace bool + // Ask user to provide namespaces to be managed by Everest. + AskNamespaces bool + // Ask user to provide DB operators to be installed into namespaces managed by Everest. + AskOperators bool helm.CLIOptions - namespaces.NamespaceAddConfig `mapstructure:",squash"` + namespaces.NamespaceAddConfig } // NewInstall returns a new Install struct. -func NewInstall(c Config, l *zap.SugaredLogger, cmd *cobra.Command) (*Install, error) { +func NewInstall(c Config, l *zap.SugaredLogger) (*Install, error) { cli := &Install{ - cmd: cmd, - l: l.With("component", "install"), + l: l.With("component", "install"), } if c.Pretty { cli.l = zap.NewNop().Sugar() @@ -119,7 +119,7 @@ func NewInstall(c Config, l *zap.SugaredLogger, cmd *cobra.Command) (*Install, e // Run the Everest installation process. func (o *Install) Run(ctx context.Context) error { - // Do not continue if Everst is already installed. + // Do not continue if Everest is already installed. installedVersion, err := version.EverestVersionFromDeployment(ctx, o.kubeClient) if client.IgnoreNotFound(err) != nil { return errors.Join(err, errors.New("cannot check if Everest is already installed")) @@ -173,16 +173,7 @@ func (o *Install) Run(ctx context.Context) error { } func (o *Install) installDBNamespacesStep(ctx context.Context) (*steps.Step, error) { - askNamespaces := !(o.cmd.Flags().Lookup(cli.FlagNamespaces).Changed || o.config.SkipDBNamespace) - askOperators := !(o.cmd.Flags().Lookup(cli.FlagOperatorMongoDB).Changed || - o.cmd.Flags().Lookup(cli.FlagOperatorPostgresql).Changed || - o.cmd.Flags().Lookup(cli.FlagOperatorXtraDBCluster).Changed) - - if askNamespaces { - o.config.Namespaces = DefaultDBNamespaceName - } - - if err := o.config.Populate(ctx, askNamespaces, askOperators); err != nil { + if err := o.config.Populate(ctx, o.config.AskNamespaces, o.config.AskOperators); err != nil { // not specifying a namespace in this context is allowed. if errors.Is(err, namespaces.ErrNSEmpty) { return nil, nil //nolint:nilnil diff --git a/pkg/cli/namespaces/add.go b/pkg/cli/namespaces/add.go index 4c95f7049..e861d5c48 100644 --- a/pkg/cli/namespaces/add.go +++ b/pkg/cli/namespaces/add.go @@ -1,3 +1,18 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 namespaces provides the functionality to manage namespaces. package namespaces @@ -71,17 +86,19 @@ func NewNamespaceAdd(c NamespaceAddConfig, l *zap.SugaredLogger) (*NamespaceAdde // NamespaceAddConfig is the configuration for adding namespaces. type NamespaceAddConfig struct { // Namespaces to install. - Namespaces string `mapstructure:"namespaces"` + Namespaces string // SkipWizard is set if the wizard should be skipped. - SkipWizard bool `mapstructure:"skip-wizard"` + SkipWizard bool // KubeconfigPath is the path to the kubeconfig file. - KubeconfigPath string `mapstructure:"kubeconfig"` + KubeconfigPath string // DisableTelemetry is set if telemetry should be disabled. - DisableTelemetry bool `mapstructure:"disable-telemetry"` + DisableTelemetry bool // TakeOwnership of an existing namespace. - TakeOwnership bool `mapstructure:"take-ownership"` + TakeOwnership bool // SkipEnvDetection skips detecting the Kubernetes environment. - SkipEnvDetection bool `mapstructure:"skip-env-detection"` + SkipEnvDetection bool + // Ask user to provide DB operators to be installed into namespaces managed by Everest. + AskOperators bool Operator OperatorConfig @@ -103,11 +120,11 @@ type NamespaceAddConfig struct { // OperatorConfig identifies which operators shall be installed. type OperatorConfig struct { // PG stores if PostgresSQL shall be installed. - PG bool `mapstructure:"postgresql"` + PG bool // PSMDB stores if MongoDB shall be installed. - PSMDB bool `mapstructure:"mongodb"` + PSMDB bool // PXC stores if XtraDB Cluster shall be installed. - PXC bool `mapstructure:"xtradb-cluster"` + PXC bool } // NamespaceAdder provides the functionality to add namespaces. @@ -127,13 +144,17 @@ func (n *NamespaceAdder) Run(ctx context.Context) error { return err } + if err := n.cfg.Populate(ctx, false, n.cfg.AskOperators); err != nil { + return err + } + if !n.skipEnvDetection { if err := n.setKubernetesEnv(ctx); err != nil { return err } } - installSteps := []steps.Step{} + var installSteps []steps.Step if version.IsDev(ver) && n.cfg.ChartDir == "" { cleanup, err := helmutils.SetupEverestDevChart(n.l, &n.cfg.ChartDir) if err != nil { diff --git a/pkg/cli/namespaces/remove.go b/pkg/cli/namespaces/remove.go index 0d636750b..41ac894a5 100644 --- a/pkg/cli/namespaces/remove.go +++ b/pkg/cli/namespaces/remove.go @@ -1,3 +1,19 @@ +// everest +// Copyright (C) 2023 Percona LLC +// +// 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 namespaces provides the functionality to manage namespaces. package namespaces import ( @@ -31,83 +47,40 @@ var ErrNamespaceNotEmpty = errors.New("cannot remove namespace with running data // NamespaceRemoveConfig is the configuration for the namespace removal operation. type NamespaceRemoveConfig struct { // KubeconfigPath is a path to a kubeconfig - KubeconfigPath string `mapstructure:"kubeconfig"` + KubeconfigPath string // Force delete a namespace by deleting databases in it. - Force bool `mapstructure:"force"` + Force bool // If set, we will keep the namespace - KeepNamespace bool `mapstructure:"keep-namespace"` + KeepNamespace bool // If set, we will print the pretty output. Pretty bool - // Namespaces (DB Namespaces) passed by user to remove. + // Namespaces (DB Namespaces managed by Everest) to remove Namespaces string // NamespaceList is a list of namespaces to remove. // This is populated internally after validating the Namespaces field.: NamespaceList []string } -// Populate the configuration with the required values. -func (cfg *NamespaceRemoveConfig) Populate(ctx context.Context) error { - if err := cfg.populateNamespaces(); err != nil { +// populate the configuration with the required values. +func (cfg *NamespaceRemoveConfig) populate(ctx context.Context, kubeClient *kubernetes.Kubernetes) error { + nsList, err := ValidateNamespaces(cfg.Namespaces) + if err != nil { return err } - for _, ns := range cfg.NamespaceList { - if err := cfg.validateNamespaceOwnership(ctx, ns); err != nil { - return fmt.Errorf("invalid namespace (%s): %w", ns, err) + for _, ns := range nsList { + // Check that the namespace exists. + exists, managedByEverest, err := namespaceExists(ctx, ns, kubeClient) + if err != nil { + return errors.Join(err, errors.New("failed to check if namespace exists")) } - - if err := cfg.validateDatabasesAbsent(ctx, ns); err != nil { - return fmt.Errorf("invalid namespace (%s): %w", ns, err) + if !exists || !managedByEverest { + return errors.New(fmt.Sprintf("namespace '%s' does not exist or not managed by Everest", ns)) } } - return nil -} - -func (cfg *NamespaceRemoveConfig) populateNamespaces() error { - namespaces := cfg.Namespaces - list, err := ValidateNamespaces(namespaces) - if err != nil { - return err - } - cfg.NamespaceList = list - return nil -} - -func (cfg *NamespaceRemoveConfig) validateNamespaceOwnership(ctx context.Context, namespace string) error { - k, err := cliutils.NewKubeclient(zap.NewNop().Sugar(), cfg.KubeconfigPath) - if err != nil { - return err - } - - nsExists, ownedByEverest, err := namespaceExists(ctx, namespace, k) - if err != nil { - return err - } - - if !nsExists { - return ErrNsDoesNotExist - } - if !ownedByEverest { - return ErrNamespaceNotManagedByEverest - } - return nil -} - -func (cfg *NamespaceRemoveConfig) validateDatabasesAbsent(ctx context.Context, namespace string) error { - k, err := cliutils.NewKubeclient(zap.NewNop().Sugar(), cfg.KubeconfigPath) - if err != nil { - return err - } - dbsExist, err := k.DatabasesExist(ctx, namespace) - if err != nil { - return errors.Join(err, errors.New("failed to check if databases exist")) - } - - if dbsExist && !cfg.Force { - return ErrNamespaceNotEmpty - } + cfg.NamespaceList = nsList return nil } @@ -144,6 +117,19 @@ func (r *NamespaceRemover) Run(ctx context.Context) error { return err } + if err := r.config.populate(ctx, r.kubeClient); err != nil { + return err + } + + dbsExist, err := r.kubeClient.DatabasesExist(ctx, r.config.NamespaceList...) + if err != nil { + return errors.Join(err, errors.New("failed to check if databases exist")) + } + + if dbsExist && !r.config.Force { + return ErrNamespaceNotEmpty + } + var removalSteps []steps.Step for _, ns := range r.config.NamespaceList { removalSteps = append(removalSteps, NewRemoveNamespaceSteps(ns, r.config.KeepNamespace, r.kubeClient)...) diff --git a/pkg/cli/uninstall/uninstall.go b/pkg/cli/uninstall/uninstall.go index 51a390ad3..fd017b671 100644 --- a/pkg/cli/uninstall/uninstall.go +++ b/pkg/cli/uninstall/uninstall.go @@ -37,13 +37,6 @@ import ( const ( pollInterval = 5 * time.Second pollTimeout = 5 * time.Minute - - // FlagCatalogNamespace is the name of the catalog namespace flag. - FlagCatalogNamespace = "catalog-namespace" - // FlagSkipEnvDetection is the name of the skip env detection flag. - FlagSkipEnvDetection = "skip-env-detection" - // FlagSkipOLM is the name of the skip OLM flag. - FlagSkipOLM = "skip-olm" ) // Uninstall implements logic for the cluster command. @@ -57,14 +50,13 @@ type Uninstall struct { // Config stores configuration for the Uninstall command. type Config struct { // KubeconfigPath is a path to a kubeconfig - KubeconfigPath string `mapstructure:"kubeconfig"` + KubeconfigPath string // AssumeYes is true when all questions can be skipped. - AssumeYes bool `mapstructure:"assume-yes"` + AssumeYes bool // Force is true when we shall not prompt for removal. Force bool // SkipEnvDetection skips detecting the Kubernetes environment. - SkipEnvDetection bool `mapstructure:"skip-env-detection"` - + SkipEnvDetection bool // If set, we will print the pretty output. Pretty bool } diff --git a/pkg/cli/upgrade/upgrade.go b/pkg/cli/upgrade/upgrade.go index 106160abf..a278fe5e2 100644 --- a/pkg/cli/upgrade/upgrade.go +++ b/pkg/cli/upgrade/upgrade.go @@ -44,26 +44,23 @@ import ( const ( pollInterval = 5 * time.Second pollTimeout = 10 * time.Minute - - // FlagSkipEnvDetection is the name of the skip env detection flag. - FlagSkipEnvDetection = "skip-env-detection" ) type ( // Config defines configuration required for upgrade command. Config struct { // KubeconfigPath is a path to a kubeconfig - KubeconfigPath string `mapstructure:"kubeconfig"` + KubeconfigPath string // InCluster is set if the upgrade process should use in-cluster configuration. - InCluster bool `mapstructure:"in-cluster"` + InCluster bool // VersionMetadataURL stores hostname to retrieve version metadata information from. - VersionMetadataURL string `mapstructure:"version-metadata-url"` + VersionMetadataURL string // DryRun is set if the upgrade process should only perform pre-upgrade checks and not perform the actual upgrade. - DryRun bool `mapstructure:"dry-run"` + DryRun bool // If set, we will print the pretty output. Pretty bool // SkipEnvDetection skips detecting the Kubernetes environment. - SkipEnvDetection bool `mapstructure:"skip-env-detection"` + SkipEnvDetection bool helm.CLIOptions } @@ -75,7 +72,6 @@ type ( config *Config kubeClient kubernetes.KubernetesConnector versionService versionservice.Interface - dryRun bool // these are set on calling Run clusterType kubernetes.ClusterType @@ -122,7 +118,6 @@ func NewUpgrade(cfg *Config, l *zap.SugaredLogger) (*Upgrade, error) { return nil, errors.New("must provide kubeconfig path or run in-cluster") } - cli.dryRun = cfg.DryRun cli.kubeClient = kubeClient cli.versionService = versionservice.New(cfg.VersionMetadataURL) return cli, nil @@ -143,13 +138,13 @@ func (u *Upgrade) Run(ctx context.Context) error { if err := u.setVersionInfo(ctx, everestVersion); err != nil { if errors.Is(err, ErrNoUpdateAvailable) { u.l.Info("You're running the latest version of Everest") - fmt.Fprintln(out, "\n", output.Rocket("You're running the latest version of Everest")) + _, _ = fmt.Fprintln(out, "\n", output.Rocket("You're running the latest version of Everest")) return nil } return err } - if u.dryRun { + if u.config.DryRun { return nil } @@ -170,7 +165,7 @@ func (u *Upgrade) Run(ctx context.Context) error { upgradeSteps := u.newUpgradeSteps() // Run steps. - fmt.Fprintln(out, output.Info("Upgrading Everest to version %s", u.upgradeToVersion)) + _, _ = fmt.Fprintln(out, output.Info("Upgrading Everest to version %s", u.upgradeToVersion)) if err := steps.RunStepsWithSpinner(ctx, upgradeSteps, out); err != nil { return err } @@ -237,11 +232,11 @@ func (u *Upgrade) setupHelmInstaller(ctx context.Context) error { } func (u *Upgrade) printPostUpgradeMessage(ctx context.Context, out io.Writer) error { - fmt.Fprintln(out, "\n", output.Rocket("Everest has been upgraded to version %s", u.upgradeToVersion)) + _, _ = fmt.Fprintln(out, "\n", output.Rocket("Everest has been upgraded to version %s", u.upgradeToVersion)) if isSecure, err := u.kubeClient.Accounts().IsSecure(ctx, common.EverestAdminUser); err != nil { return errors.Join(err, errors.New("could not check if the admin password is secure")) } else if !isSecure { - fmt.Fprint(os.Stdout, "\n", common.InitialPasswordWarningMessage, "\n") + _, _ = fmt.Fprint(os.Stdout, "\n", common.InitialPasswordWarningMessage, "\n") } return nil } diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index d991925f9..364162853 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -20,7 +20,6 @@ import ( "fmt" "time" - "github.com/spf13/cobra" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -28,7 +27,7 @@ import ( ) // MustInitLogger initializes logger and panics in case of an error. // FIXME Test this. -func MustInitLogger(json bool) *zap.Logger { +func MustInitLogger(json bool, logName string) *zap.Logger { var loggerCfg zap.Config if config.Debug { loggerCfg = zap.NewDevelopmentConfig() @@ -47,45 +46,40 @@ func MustInitLogger(json bool) *zap.Logger { } loggerCfg.DisableStacktrace = true - logger, err := loggerCfg.Build() - if err != nil { - panic(fmt.Sprintf("Cannot initialize logger: %s", err)) - } - - return logger + return initGlobalLogger(loggerCfg, logName) } // MustInitVerboseLogger initializes a verbose logger and panics in case of an error. -func MustInitVerboseLogger(json bool) *zap.Logger { +func MustInitVerboseLogger(json bool, logName string) *zap.Logger { lCfg := zap.NewDevelopmentConfig() if json { lCfg.Encoding = "json" } - - l, err := lCfg.Build() - if err != nil { - panic(fmt.Sprintf("Cannot initialize logger: %s", err)) - } - - return l + return initGlobalLogger(lCfg, logName) } // InitLoggerInRootCmd inits the provided logger instance based on command's flags. // This is meant to be run by the root command in PersistentPreRun step. -func InitLoggerInRootCmd(cmd *cobra.Command, l *zap.SugaredLogger) { - verbose, err := cmd.Flags().GetBool("verbose") - if err != nil { - l.Warn(`Could not parse "verbose" flag`) +func InitLoggerInRootCmd(verbose, json bool, logName string) { + if verbose { + MustInitVerboseLogger(json, logName) + } else { + MustInitLogger(json, logName) } +} + +// GetLogger returns the global logger instance. +func GetLogger() *zap.SugaredLogger { + return zap.L().Sugar() +} - json, err := cmd.Flags().GetBool("json") +func initGlobalLogger(loggerCfg zap.Config, logName string) *zap.Logger { + logger, err := loggerCfg.Build() if err != nil { - l.Warn(`Could not parse "json" flag`) + panic(fmt.Sprintf("Cannot initialize logger: %s", err)) } - if verbose { - *l = *MustInitVerboseLogger(json).Sugar() - } else if json { - *l = *MustInitLogger(true).Sugar() - } + zap.ReplaceGlobals(logger.Named(logName)) + + return zap.L() } diff --git a/pkg/oidc/configure.go b/pkg/oidc/configure.go index 58c4685a3..cdb885455 100644 --- a/pkg/oidc/configure.go +++ b/pkg/oidc/configure.go @@ -23,6 +23,7 @@ import ( "github.com/AlecAivazis/survey/v2" "go.uber.org/zap" + cliutils "github.com/percona/everest/pkg/cli/utils" "github.com/percona/everest/pkg/common" "github.com/percona/everest/pkg/kubernetes" ) @@ -37,25 +38,32 @@ type OIDC struct { // Config stores configuration for the OIDC command. type Config struct { // KubeconfigPath is a path to a kubeconfig - KubeconfigPath string `mapstructure:"kubeconfig"` + KubeconfigPath string + // Pretty print the output. + Pretty bool // IssuerURL OIDC issuer url. - IssuerURL string `mapstructure:"issuer-url"` + IssuerURL string // ClientID ID of the client OIDC app. - ClientID string `mapstructure:"client-id"` + ClientID string } // NewOIDC returns a new OIDC struct. func NewOIDC(c Config, l *zap.SugaredLogger) (*OIDC, error) { - kubeClient, err := kubernetes.New(c.KubeconfigPath, l) + cli := &OIDC{ + config: c, + l: l.With("component", "oidc"), + } + + if c.Pretty { + cli.l = zap.NewNop().Sugar() + } + + k, err := cliutils.NewKubeclient(cli.l, c.KubeconfigPath) if err != nil { return nil, err } + cli.kubeClient = k - cli := &OIDC{ - config: c, - kubeClient: kubeClient, - l: l, - } return cli, nil } diff --git a/pkg/oidc/oidc.go b/pkg/oidc/oidc.go index 5e4616e20..ed17241f3 100644 --- a/pkg/oidc/oidc.go +++ b/pkg/oidc/oidc.go @@ -68,7 +68,7 @@ func getProviderConfig(ctx context.Context, issuer string) (ProviderConfig, erro var result ProviderConfig if err := json.Unmarshal(body, &result); err != nil { - return ProviderConfig{}, fmt.Errorf("failed to unmarshal JSON response: %w", err) + return ProviderConfig{}, fmt.Errorf("failed to unmarshal json response: %w", err) } return result, nil } diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 2205bbf0e..44ef0b0ff 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -62,12 +62,15 @@ const ( ActionRead = "read" ActionUpdate = "update" ActionDelete = "delete" + ActionAll = "*" ) const ( rbacEnabledValueTrue = "true" ) +var SupportedActions = []string{ActionCreate, ActionRead, ActionUpdate, ActionDelete, ActionAll} + // Setup a new informer that watches our RBAC ConfigMap. // This informer reloads the policy whenever the ConfigMap is updated. func refreshEnforcerInBackground( @@ -299,3 +302,8 @@ func IsEnabled(cm *corev1.ConfigMap) bool { func ObjectName(args ...string) string { return strings.Join(args, "/") } + +// ValidateAction validates the action is supported. +func ValidateAction(action string) bool { + return slices.Contains(SupportedActions, action) +} diff --git a/pkg/version/version.go b/pkg/version/version.go index dc63f0e9b..647caf563 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -74,7 +74,7 @@ func (v Info) String() string { return strings.Join(out, "\n") } -// JSONString returns the JSON representation of the version information. +// JSONString returns the json representation of the version information. func (v Info) JSONString() string { data, err := json.Marshal(v) if err != nil {