From ff0c7e1eef3c0da186244eea01d4ceb9e63ad45d Mon Sep 17 00:00:00 2001 From: facchettos Date: Tue, 6 Aug 2024 09:30:39 +0200 Subject: [PATCH] [UX] moved login from root level to platform subcommand --- cmd/vclusterctl/cmd/login.go | 1 + cmd/vclusterctl/cmd/logout.go | 1 + cmd/vclusterctl/cmd/platform/login.go | 288 +++++++++++++++++++++++ cmd/vclusterctl/cmd/platform/logout.go | 76 ++++++ cmd/vclusterctl/cmd/platform/platform.go | 10 + 5 files changed, 376 insertions(+) create mode 100644 cmd/vclusterctl/cmd/platform/login.go create mode 100644 cmd/vclusterctl/cmd/platform/logout.go diff --git a/cmd/vclusterctl/cmd/login.go b/cmd/vclusterctl/cmd/login.go index 36677658f1..4482094bc3 100644 --- a/cmd/vclusterctl/cmd/login.go +++ b/cmd/vclusterctl/cmd/login.go @@ -64,6 +64,7 @@ vcluster login https://my-vcluster-platform.com --access-key myaccesskey Long: description, Args: cobra.MaximumNArgs(1), RunE: func(cobraCmd *cobra.Command, args []string) error { + log.GetInstance().Warnf("\"vcluster login\" is deprecated, please use \"vcluster platform login\" instead") // Check for newer version upgrade.PrintNewerVersionWarning() diff --git a/cmd/vclusterctl/cmd/logout.go b/cmd/vclusterctl/cmd/logout.go index 2b6e7de1e2..52f0ba70fa 100644 --- a/cmd/vclusterctl/cmd/logout.go +++ b/cmd/vclusterctl/cmd/logout.go @@ -42,6 +42,7 @@ vcluster logout Long: description, Args: cobra.NoArgs, RunE: func(cobraCmd *cobra.Command, _ []string) error { + log.GetInstance().Warnf("\"vcluster logout\" is deprecated, please use \"vcluster platform logout\" instead") return cmd.Run(cobraCmd.Context()) }, } diff --git a/cmd/vclusterctl/cmd/platform/login.go b/cmd/vclusterctl/cmd/platform/login.go new file mode 100644 index 0000000000..3c4e0bfa6a --- /dev/null +++ b/cmd/vclusterctl/cmd/platform/login.go @@ -0,0 +1,288 @@ +package platform + +import ( + "bytes" + "context" + "fmt" + "os" + "strings" + + dockerconfig "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/configfile" + managementv1 "github.com/loft-sh/api/v4/pkg/apis/management/v1" + storagev1 "github.com/loft-sh/api/v4/pkg/apis/storage/v1" + "github.com/loft-sh/api/v4/pkg/product" + "github.com/loft-sh/log" + "github.com/loft-sh/vcluster/cmd/vclusterctl/cmd/use" + "github.com/loft-sh/vcluster/pkg/cli/config" + "github.com/loft-sh/vcluster/pkg/cli/flags" + "github.com/loft-sh/vcluster/pkg/docker" + "github.com/loft-sh/vcluster/pkg/platform" + "github.com/loft-sh/vcluster/pkg/platform/clihelper" + "github.com/loft-sh/vcluster/pkg/platform/kube" + "github.com/loft-sh/vcluster/pkg/upgrade" + "github.com/mgutz/ansi" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const PlatformURL = "VCLUSTER_PLATFORM_URL" + +type LoginCmd struct { + *flags.GlobalFlags + + Log log.Logger + + Driver string + AccessKey string + Insecure bool + DockerLogin bool +} + +func NewLoginCmd(globalFlags *flags.GlobalFlags) (*cobra.Command, error) { + cmd := LoginCmd{ + GlobalFlags: globalFlags, + Log: log.GetInstance(), + } + + description := `######################################################## +############### vcluster platform login ################ +######################################################## +Login into vCluster platform + +Example: +vcluster platform login https://my-vcluster-platform.com +vcluster platform login https://my-vcluster-platform.com --access-key myaccesskey +######################################################## + ` + + loginCmd := &cobra.Command{ + Use: "login [VCLUSTER_PLATFORM_HOST]", + Short: "Login to a vCluster platform instance", + Long: description, + Args: cobra.MaximumNArgs(1), + RunE: func(cobraCmd *cobra.Command, args []string) error { + // Check for newer version + upgrade.PrintNewerVersionWarning() + + return cmd.Run(cobraCmd.Context(), args) + }, + } + + loginCmd.Flags().StringVar(&cmd.Driver, "use-driver", "", "Switch vCluster driver between platform and helm") + loginCmd.Flags().StringVar(&cmd.AccessKey, "access-key", "", "The access key to use") + loginCmd.Flags().BoolVar(&cmd.Insecure, "insecure", true, product.Replace("Allow login into an insecure Loft instance")) + loginCmd.Flags().BoolVar(&cmd.DockerLogin, "docker-login", true, "If true, will log into the docker image registries the user has image pull secrets for") + + return loginCmd, nil +} + +func (cmd *LoginCmd) Run(ctx context.Context, args []string) error { + cfg := cmd.LoadedConfig(cmd.Log) + + var url string + // Print login information + if len(args) == 0 { + url = os.Getenv(PlatformURL) + if url == "" { + insecureFlag := "" + if cfg.Platform.Insecure { + insecureFlag = "--insecure" + } + + err := cmd.printLoginDetails(ctx) + if err != nil { + cmd.Log.Fatalf("%s\n\nYou may need to log in again via: %s platform login %s %s\n", err.Error(), os.Args[0], cfg.Platform.Host, insecureFlag) + } + + domain := cfg.Platform.Host + if domain == "" { + domain = "my-vcluster-platform.com" + } + + cmd.Log.WriteString(logrus.InfoLevel, fmt.Sprintf("\nTo log in as a different user, run: %s platform login %s %s\n\n", os.Args[0], domain, insecureFlag)) + + return nil + } + } else { + url = args[0] + } + + if !strings.HasPrefix(url, "http") { + url = "https://" + url + } + + // log into platform + loginClient := platform.NewLoginClientFromConfig(cfg) + url = strings.TrimSuffix(url, "/") + var err error + if cmd.AccessKey != "" { + err = loginClient.LoginWithAccessKey(url, cmd.AccessKey, cmd.Insecure) + } else { + err = loginClient.Login(url, cmd.Insecure, cmd.Log) + } + if err != nil { + return err + } + cmd.Log.Donef(product.Replace("Successfully logged into Loft instance %s"), ansi.Color(url, "white+b")) + + // skip log into docker registries? + if !cmd.DockerLogin { + return nil + } + + err = dockerLogin(ctx, cmd.LoadedConfig(cmd.Log), cmd.Log) + if err != nil { + return err + } + + // should switch driver + if cmd.Driver != "" { + err := use.SwitchDriver(ctx, cfg, cmd.Driver, log.GetInstance()) + if err != nil { + return fmt.Errorf("driver switch failed: %w", err) + } + } + + return nil +} + +func (cmd *LoginCmd) printLoginDetails(ctx context.Context) error { + cfg := cmd.LoadedConfig(cmd.Log) + platformClient := platform.NewClientFromConfig(cfg) + + managementClient, err := platformClient.Management() + if err != nil { + return err + } + + userName, teamName, err := platform.GetCurrentUser(ctx, managementClient) + if err != nil { + return err + } + + if userName != nil { + cmd.Log.Infof("Logged into %s as user: %s", platformClient.Config().Platform.Host, clihelper.DisplayName(&userName.EntityInfo)) + } else { + cmd.Log.Infof("Logged into %s as team: %s", platformClient.Config().Platform.Host, clihelper.DisplayName(teamName)) + } + return nil +} + +func dockerLogin(ctx context.Context, config *config.CLI, log log.Logger) error { + platformClient := platform.NewClientFromConfig(config) + + managementClient, err := platformClient.Management() + if err != nil { + return err + } + + // get user name + userName, teamName, err := platform.GetCurrentUser(ctx, managementClient) + if err != nil { + return err + } + + // collect image pull secrets from team or user + dockerConfigs := []*configfile.ConfigFile{} + if userName != nil { + // get image pull secrets from user + user, err := managementClient.Loft().ManagementV1().Users().Get(ctx, userName.Name, metav1.GetOptions{}) + if err != nil { + return err + } + dockerConfigs = append(dockerConfigs, collectImagePullSecrets(ctx, managementClient, user.Spec.ImagePullSecrets, log)...) + + // get image pull secrets from teams + for _, teamName := range user.Status.Teams { + team, err := managementClient.Loft().ManagementV1().Teams().Get(ctx, teamName, metav1.GetOptions{}) + if err != nil { + return err + } + + dockerConfigs = append(dockerConfigs, collectImagePullSecrets(ctx, managementClient, team.Spec.ImagePullSecrets, log)...) + } + } else if teamName != nil { + // get image pull secrets from team + team, err := managementClient.Loft().ManagementV1().Teams().Get(ctx, teamName.Name, metav1.GetOptions{}) + if err != nil { + return err + } + dockerConfigs = append(dockerConfigs, collectImagePullSecrets(ctx, managementClient, team.Spec.ImagePullSecrets, log)...) + } + + // store docker configs + if len(dockerConfigs) > 0 { + dockerConfig, err := docker.NewDockerConfig() + if err != nil { + return err + } + + // log into registries locally + for _, config := range dockerConfigs { + for registry, authConfig := range config.AuthConfigs { + err = dockerConfig.Store(registry, authConfig) + if err != nil { + return err + } + + if registry == "https://index.docker.io/v1/" { + registry = "docker hub" + } + + log.Donef("Successfully logged into docker registry '%s'", registry) + } + } + + err = dockerConfig.Save() + if err != nil { + return errors.Wrap(err, "save docker config") + } + } + + return nil +} + +func collectImagePullSecrets(ctx context.Context, managementClient kube.Interface, imagePullSecrets []*storagev1.KindSecretRef, log log.Logger) []*configfile.ConfigFile { + retConfigFiles := []*configfile.ConfigFile{} + for _, imagePullSecret := range imagePullSecrets { + // unknown image pull secret type? + if imagePullSecret.Kind != "SharedSecret" || (imagePullSecret.APIGroup != storagev1.SchemeGroupVersion.Group && imagePullSecret.APIGroup != managementv1.SchemeGroupVersion.Group) { + continue + } else if imagePullSecret.SecretName == "" || imagePullSecret.SecretNamespace == "" { + continue + } + + sharedSecret, err := managementClient.Loft().ManagementV1().SharedSecrets(imagePullSecret.SecretNamespace).Get(ctx, imagePullSecret.SecretName, metav1.GetOptions{}) + if err != nil { + log.Warnf("Unable to retrieve image pull secret %s/%s: %v", imagePullSecret.SecretNamespace, imagePullSecret.SecretName, err) + continue + } else if len(sharedSecret.Spec.Data) == 0 { + log.Warnf("Unable to retrieve image pull secret %s/%s: secret is empty", imagePullSecret.SecretNamespace, imagePullSecret.SecretName) + continue + } else if imagePullSecret.Key == "" && len(sharedSecret.Spec.Data) > 1 { + log.Warnf("Unable to retrieve image pull secret %s/%s: secret has multiple keys, but none is specified for image pull secret", imagePullSecret.SecretNamespace, imagePullSecret.SecretName) + continue + } + + // determine shared secret key + key := imagePullSecret.Key + if key == "" { + for k := range sharedSecret.Spec.Data { + key = k + } + } + + configFile, err := dockerconfig.LoadFromReader(bytes.NewReader(sharedSecret.Spec.Data[key])) + if err != nil { + log.Warnf("Parsing image pull secret %s/%s.%s: %v", imagePullSecret.SecretNamespace, imagePullSecret.SecretName, key, err) + continue + } + + retConfigFiles = append(retConfigFiles, configFile) + } + + return retConfigFiles +} diff --git a/cmd/vclusterctl/cmd/platform/logout.go b/cmd/vclusterctl/cmd/platform/logout.go new file mode 100644 index 0000000000..08ea6b0216 --- /dev/null +++ b/cmd/vclusterctl/cmd/platform/logout.go @@ -0,0 +1,76 @@ +package platform + +import ( + "context" + "fmt" + + "github.com/loft-sh/api/v4/pkg/product" + "github.com/loft-sh/log" + "github.com/loft-sh/vcluster/cmd/vclusterctl/cmd/use" + "github.com/loft-sh/vcluster/pkg/cli/config" + "github.com/loft-sh/vcluster/pkg/cli/flags" + "github.com/loft-sh/vcluster/pkg/platform" + "github.com/mgutz/ansi" + "github.com/spf13/cobra" +) + +type LogoutCmd struct { + *flags.GlobalFlags + + Log log.Logger +} + +func NewLogoutCmd(globalFlags *flags.GlobalFlags) (*cobra.Command, error) { + cmd := &LogoutCmd{ + GlobalFlags: globalFlags, + Log: log.GetInstance(), + } + + description := `######################################################## +############## vcluster platform logout ################ +######################################################## +Log out of vCluster platform + +Example: +vcluster platform logout +######################################################## + ` + + logoutCmd := &cobra.Command{ + Use: "logout", + Short: "Log out of a vCluster platform instance", + Long: description, + Args: cobra.NoArgs, + RunE: func(cobraCmd *cobra.Command, _ []string) error { + return cmd.Run(cobraCmd.Context()) + }, + } + + return logoutCmd, nil +} + +func (cmd *LogoutCmd) Run(ctx context.Context) error { + platformClient := platform.NewClientFromConfig(cmd.LoadedConfig(cmd.Log)) + + // delete old access key if were logged in before + cfg := platformClient.Config() + if cfg.Platform.AccessKey != "" { + if err := platformClient.Logout(ctx); err != nil { + cmd.Log.Errorf("failed to send logout request: %v", err) + } + + configHost := cfg.Platform.Host + cfg.Platform.Host = "" + cfg.Platform.AccessKey = "" + cfg.Platform.LastInstallContext = "" + cfg.Platform.Insecure = false + + if err := platformClient.Save(); err != nil { + return fmt.Errorf("save config: %w", err) + } + + cmd.Log.Donef(product.Replace("Successfully logged out of loft instance %s"), ansi.Color(configHost, "white+b")) + } + + return use.SwitchDriver(ctx, cfg, string(config.HelmDriver), cmd.Log) +} diff --git a/cmd/vclusterctl/cmd/platform/platform.go b/cmd/vclusterctl/cmd/platform/platform.go index 2eaa663cbd..d8d2ce2cec 100644 --- a/cmd/vclusterctl/cmd/platform/platform.go +++ b/cmd/vclusterctl/cmd/platform/platform.go @@ -48,6 +48,14 @@ func NewPlatformCmd(globalFlags *flags.GlobalFlags) (*cobra.Command, error) { } startCmd := NewStartCmd(globalFlags) + loginCmd, err := NewLoginCmd(globalFlags) + if err != nil { + return nil, err + } + logoutCmd, err := NewLogoutCmd(globalFlags) + if err != nil { + return nil, err + } platformCmd.AddCommand(startCmd) platformCmd.AddCommand(NewResetCmd(globalFlags)) @@ -63,6 +71,8 @@ func NewPlatformCmd(globalFlags *flags.GlobalFlags) (*cobra.Command, error) { platformCmd.AddCommand(share.NewShareCmd(globalFlags, defaults)) platformCmd.AddCommand(create.NewCreateCmd(globalFlags, defaults)) platformCmd.AddCommand(cmddelete.NewDeleteCmd(globalFlags, defaults)) + platformCmd.AddCommand(loginCmd) + platformCmd.AddCommand(logoutCmd) return platformCmd, nil }