diff --git a/docs/quickstart/quickstart.md b/docs/quickstart/quickstart.md index 0079d403f..b28a91726 100644 --- a/docs/quickstart/quickstart.md +++ b/docs/quickstart/quickstart.md @@ -177,6 +177,72 @@ tanzu plugin upgrade [--target ] This command will update the specified plugin to the recommendedVersion associated with this plugin's entry found in the plugin repository. +### Logging into Tanzu Application Platform + +To log in to the Tanzu Application Platform and create a context of type tanzu, use the following command: + +```console +tanzu login +``` + +This command will log in to the Tanzu Application Platform and creates a context associated +with the organization's name that you are logging in to. Once the context is created, +you can manage it using the `tanzu context` command. + +After logging in, you can use the tanzu login command again to update the authentication aspects +of the existing context, while keeping the rest of the context data intact. + +#### Tanzu login + +User can log in using interactive login (default mechanism) or by utilizing an API Token. + +##### Interactive Login + +By default, CLI uses interactive login. The CLI opens the browser for the user to log in. +CLI will attempt to log in interactively to the user's default Cloud Services organization. You can override or choose a +custom organization by setting the `TANZU_CLI_CLOUD_SERVICES_ORGANIZATION_ID` environment variable with the custom +organization ID value. More information regarding organizations in Cloud Services and how to obtain the organization ID +can be +found [here](https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-CF9E9318-B811-48CF-8499-9419997DC1F8.html). + +After successful authentication, a context of type "tanzu" is created. + +Example command for interactive login: + +```console +tanzu login +``` + +Notes: + +- For terminal hosts without a browser, users can set the `TANZU_CLI_OAUTH_LOCAL_LISTENER_PORT` environment variable + with a chosen port number. Then, run the `tanzu login` command. + The CLI will show an OAuth URL link in the console and start a local listener on the specified port. + Users can use SSH port forwarding to forward the port on their machine to the terminal/server machine where the + local listener is running. Once the port forwarding is initiated, the user can open the OAuth URL in their local + machine browser to complete the login and create a Tanzu context. + +- Alternatively, users can run the `tanzu login` command in the + terminal host. The CLI will display the OAuth URL link in the console and provide an option for the user to paste + the Auth code from the browser URL([Image reference](./images/interactive_login_copy_authcode.png)) to the + console. + +##### API Token + +Example command to log in using an API token: + +```console +TANZU_API_TOKEN= tanzu login +``` + +Users can persist the environment variable in the CLI configuration file, which will be used for each CLI command +invocation: + +```console +tanzu config set env.TANZU_API_TOKEN +tanzu login +``` + ### Creating and connecting to a new context ```console @@ -288,26 +354,24 @@ updated. Below is the summary of the changes to expect: -### tanzu login +### Deprecated tanzu login Command + +The `tanzu login` command has undergone significant changes. -Two important changes apply to the `tanzu login` command. +- **Formerly a Plugin:** Initially, this command was provided by the login plugin + and would appear in the list generated by `tanzu plugin list`. However, it has + now been transitioned to a core CLI command. -First, this command used to be provided by the `login` plugin. The -`tanzu plugin list` command would therefore show the `login` plugin, if it was -installed. This is no longer the case as the `tanzu login` command is now a -core CLI command. The `tanzu plugin list` command will therefore no longer -show the `login` plugin. An exception to the last statement is if the `login` -plugin was previously installed through a legacy version of the CLI. +- **Deprecation**: The core `tanzu login` command was deprecated in favor of + the `tanzu context create` or `tanzu context use` commands for managing contexts. -If the user needs to continue using any legacy Tanzu CLI for a while, -it is preferable to keep the `login` plugin installed, as it is needed -for such CLIs. Once the user no longer needs any legacy CLI, they -can run `tanzu plugin uninstall login` to remove this plugin for the new CLI. +- **Repurposing:** The deprecated `tanzu login` core command has been repurposed + to facilitate logging into the Tanzu Application Platform. It is no longer + considered deprecated. -The second change to the `tanzu login` command is that it is now deprecated -and therefore no longer shown in the help text when running `tanzu -h`. -The `tanzu context create` or `tanzu context use` commands should be used -instead of the deprecated `tanzu login` command. +- **For Legacy CLI Users:** If you still require the use of any legacy Tanzu CLI, + it is advisable to retain the `login plugin`. Once you no longer need the legacy CLI, + you can uninstall the plugin using `tanzu plugin uninstall login`. ### tanzu config server diff --git a/pkg/command/ceip_participation_test.go b/pkg/command/ceip_participation_test.go index 5068aad07..0e9b29e24 100644 --- a/pkg/command/ceip_participation_test.go +++ b/pkg/command/ceip_participation_test.go @@ -41,7 +41,7 @@ var _ = Describe("ceip-participation command tests", func() { os.Unsetenv("TANZU_CONFIG_NEXT_GEN") os.RemoveAll(tkgConfigFile.Name()) os.RemoveAll(tkgConfigFileNG.Name()) - resetLoginCommandFlags() + resetContextCommandFlags() }) Context("ceip-participation set to true", func() { It("ceip-participation set should be successful and get should return status as 'Opt-in' status", func() { diff --git a/pkg/command/context.go b/pkg/command/context.go index 48206ea14..104171409 100644 --- a/pkg/command/context.go +++ b/pkg/command/context.go @@ -264,7 +264,7 @@ func createCtx(cmd *cobra.Command, args []string) (err error) { err = k8sLogin(ctx) } else if ctx.ContextType == configtypes.ContextTypeTanzu { // Tanzu control plane login - err = globalTanzuLogin(ctx) + err = globalTanzuLogin(ctx, nil) } else { err = globalLogin(ctx) } @@ -281,6 +281,8 @@ func createCtx(cmd *cobra.Command, args []string) (err error) { // syncContextPlugins syncs the plugins for the given context type // if listPlugins is true, it will list the plugins that will be installed for the given context type +// +//nolint:unparam func syncContextPlugins(cmd *cobra.Command, contextType configtypes.ContextType, ctxName string, listPlugins bool) error { plugins, err := pluginmanager.DiscoverPluginsForContextType(contextType) errList := make([]error, 0) @@ -617,70 +619,111 @@ func globalLogin(c *configtypes.Context) (err error) { } // format - fmt.Println() + fmt.Fprintln(os.Stderr) log.Success("successfully created a TMC context") return nil } -func globalTanzuLogin(c *configtypes.Context) error { - var claims *csp.Claims - var err error - apiTokenValue, apiTokenExists := os.LookupEnv(config.EnvAPITokenKey) - // Use API Token login flow if TANZU_API_TOKEN environment variable is set, else fall back to default interactive login flow - if apiTokenExists { - log.Info("API token env var is set") - claims, err = doCSPAPITokenAuthAndUpdateContext(c, apiTokenValue) - } else { - claims, err = doCSPInteractiveLoginAndUpdateContext(c) - } +func globalTanzuLogin(c *configtypes.Context, generateContextNameFunc func(orgName, endpoint string, isStaging bool) string) error { + claims, err := doCSPAuthentication(c) if err != nil { return err } - c.AdditionalMetadata[config.OrgIDKey] = claims.OrgID - kubeCfg, kubeCtx, serverEndpoint, err := tanzuauth.GetTanzuKubeconfig(c, endpoint, claims.OrgID, endpointCACertPath, skipTLSVerify) + orgName, err := getCSPOrganizationName(c, claims) if err != nil { return err } + // update the context name using the context name generator + if generateContextNameFunc != nil { + c.Name = generateContextNameFunc(orgName, endpoint, staging) + } - c.ClusterOpts.Path = kubeCfg - c.ClusterOpts.Context = kubeCtx - c.ClusterOpts.Endpoint = serverEndpoint + // update the context metadata + if err := updateTanzuContextMetadata(c, claims.OrgID, orgName); err != nil { + return err + } - err = config.AddContext(c, true) - if err != nil { + // Fetch the tanzu kubeconfig and update context + if err := updateContextWithTanzuKubeconfig(c, endpoint, claims.OrgID, endpointCACertPath, skipTLSVerify); err != nil { + return err + } + + // Add the context to configuration + if err := config.AddContext(c, true); err != nil { return err } + // update the current context in the kubeconfig file after creating the context - err = syncCurrentKubeContext(c) - if err != nil { + if err := syncCurrentKubeContext(c); err != nil { return errors.Wrap(err, "unable to update current kube context") } // format - fmt.Println() - orgName := getCSPOrgName(c, claims) - // If the orgName fetching API fails(corner case), we only print the tanzu context creation success message - msg := "Successfully created a tanzu context" - if orgName != "" { - msg = fmt.Sprintf("Successfully logged into '%s' organization and created a tanzu context", orgName) + fmt.Fprintln(os.Stderr) + log.Successf("Successfully logged into '%s' organization and created a tanzu context", orgName) + return nil +} + +// updateTanzuContextMetadata updates the context additional metadata +func updateTanzuContextMetadata(c *configtypes.Context, orgID, orgName string) error { + exists, err := config.ContextExists(c.Name) + if err != nil { + return err } - log.Success(msg) + if !exists { + c.AdditionalMetadata[config.OrgIDKey] = orgID + c.AdditionalMetadata[config.OrgNameKey] = orgName + return nil + } + // This is possible only for contexts created using "tanzu login" command because + // "tanzu context create" command doesn't allow user to create duplicate contexts + existingContext, err := config.GetContext(c.Name) + if err != nil { + return err + } + // If the context exists with the same name, honor the users current context additional metadata + // which includes the org/project/space details. + c.AdditionalMetadata = existingContext.AdditionalMetadata + return nil } -// getCSPOrgName returns the CSP Org name using the orgID from the claims. +// getCSPOrganizationName returns the CSP Org name using the orgID from the claims. // It will return empty string if API fails -func getCSPOrgName(c *configtypes.Context, claims *csp.Claims) string { +func getCSPOrganizationName(c *configtypes.Context, claims *csp.Claims) (string, error) { issuer := csp.ProdIssuer if staging { issuer = csp.StgIssuer } orgName, err := csp.GetOrgNameFromOrgID(claims.OrgID, c.GlobalOpts.Auth.AccessToken, issuer) if err != nil { - return "" + return "", err } - return orgName + return orgName, nil +} + +func updateContextWithTanzuKubeconfig(c *configtypes.Context, ep, orgID, epCACertPath string, skipTLSVerify bool) error { + kubeCfg, kubeCtx, orgEndpoint, err := tanzuauth.GetTanzuKubeconfig(c, ep, orgID, epCACertPath, skipTLSVerify) + if err != nil { + return err + } + c.ClusterOpts.Path = kubeCfg + c.ClusterOpts.Context = kubeCtx + // for "tanzu" context ClusterOpts.Endpoint would always be pointing to UCP organization endpoint + c.ClusterOpts.Endpoint = orgEndpoint + + return nil +} + +func doCSPAuthentication(c *configtypes.Context) (*csp.Claims, error) { + apiTokenValue, apiTokenExists := os.LookupEnv(config.EnvAPITokenKey) + // Use API Token login flow if TANZU_API_TOKEN environment variable is set, else fall back to default interactive login flow + if apiTokenExists { + log.Info("API token env var is set") + return doCSPAPITokenAuthAndUpdateContext(c, apiTokenValue) + } + return doCSPInteractiveLoginAndUpdateContext(c) } func doCSPInteractiveLoginAndUpdateContext(c *configtypes.Context) (claims *csp.Claims, err error) { @@ -1567,12 +1610,7 @@ func setTanzuCtxActiveResource(_ *cobra.Command, args []string) error { if err != nil { return errors.Wrap(err, "failed updating the context %q with the active tanzu resource") } - // TODO (prkalle): Adding this fallback logic to support the backward compatibility. This should be updated to use projectID for official release - // If the projectIDStr is set it would be used for kubeconfig generation, else use the project name - projectVal := projectStr - if projectIDStr != "" { - projectVal = projectIDStr - } + projectVal := getProjectValueForKubeconfig(projectStr, projectIDStr) err = updateTanzuContextKubeconfig(ctx, projectVal, spaceStr, clustergroupStr) if err != nil { return errors.Wrap(err, "failed to update the tanzu context kubeconfig") @@ -1581,6 +1619,18 @@ func setTanzuCtxActiveResource(_ *cobra.Command, args []string) error { return nil } +// getProjectValueForKubeconfig return the project value to be used for UCP kubeconfig generation. +// +// Note: This method can be removed for official release as projectID would be used for kubeconfig generation. +func getProjectValueForKubeconfig(projectName, projectID string) string { + // TODO (prkalle): Adding this fallback logic to support the backward compatibility. This should be updated to use projectID for official release + // If the projectIDStr is set it would be used for kubeconfig generation, else use the project name + if projectID != "" { + return projectID + } + return projectName +} + func validateActiveResourceOptions() error { if spaceStr != "" && clustergroupStr != "" { return errors.Errorf("either space or clustergroup can be set as active resource. Please provide either --space or --clustergroup option") diff --git a/pkg/command/context_test.go b/pkg/command/context_test.go index aa4456a29..43b2b2d27 100644 --- a/pkg/command/context_test.go +++ b/pkg/command/context_test.go @@ -691,6 +691,7 @@ clusterOpts: }) Describe("tanzu context unset", func() { + var name string cmd := &cobra.Command{} BeforeEach(func() { targetStr = "" diff --git a/pkg/command/login.go b/pkg/command/login.go index a52bf3c5b..92d3efd15 100644 --- a/pkg/command/login.go +++ b/pkg/command/login.go @@ -4,434 +4,142 @@ package command import ( + "crypto/sha256" + "encoding/hex" "fmt" - "os" - "sort" "strings" - "time" + "github.com/google/uuid" "github.com/spf13/cobra" - "golang.org/x/oauth2" - "github.com/vmware-tanzu/tanzu-plugin-runtime/component" "github.com/vmware-tanzu/tanzu-plugin-runtime/config" configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" - "github.com/vmware-tanzu/tanzu-plugin-runtime/log" "github.com/vmware-tanzu/tanzu-plugin-runtime/plugin" - "github.com/vmware-tanzu/tanzu-cli/pkg/auth/csp" - tkgauth "github.com/vmware-tanzu/tanzu-cli/pkg/auth/tkg" - kubecfg "github.com/vmware-tanzu/tanzu-cli/pkg/auth/utils/kubeconfig" "github.com/vmware-tanzu/tanzu-cli/pkg/cli" - "github.com/vmware-tanzu/tanzu-cli/pkg/pluginmanager" + "github.com/vmware-tanzu/tanzu-cli/pkg/utils" ) -var ( - name, server string -) +var loginEndpoint string var loginCmd = &cobra.Command{ Use: "login", - Short: "Login to the platform", + Short: "Login to Tanzu Application Platform", Aliases: []string{"lo", "logins"}, Annotations: map[string]string{ "group": string(plugin.SystemCmdGroup), }, - RunE: login, + ValidArgsFunction: noMoreCompletions, + RunE: login, } func init() { - loginCmd.Flags().StringVar(&endpoint, "endpoint", "", "endpoint to login to") - loginCmd.Flags().StringVar(&name, "name", "", "name of the server") - loginCmd.Flags().StringVar(&apiToken, "apiToken", "", "API token for global login") - loginCmd.Flags().StringVar(&server, "server", "", "login to the given server") - loginCmd.Flags().StringVar(&kubeConfig, "kubeconfig", "", "path to kubeconfig management cluster. Valid only if user doesn't choose 'endpoint' option.(See [*])") - loginCmd.Flags().StringVar(&kubeContext, "context", "", "the context in the kubeconfig to use for management cluster. Valid only if user doesn't choose 'endpoint' option.(See [*]) ") - loginCmd.Flags().BoolVar(&stderrOnly, "stderr-only", false, "send all output to stderr rather than stdout") - loginCmd.Flags().BoolVar(&forceCSP, "force-csp", false, "force the endpoint to be logged in as a csp server") + // "endpoint" variable from context.go cannot be used as default value varies for login command + loginCmd.Flags().StringVar(&loginEndpoint, "endpoint", "https://api.tanzu.cloud.vmware.com", "endpoint to login to") + utils.PanicOnErr(loginCmd.RegisterFlagCompletionFunc("endpoint", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return cobra.AppendActiveHelp(nil, "Please enter the endpoint for which to create the context"), cobra.ShellCompDirectiveNoFileComp + })) + loginCmd.Flags().StringVar(&apiToken, "api-token", "", "API token for the SaaS login") + utils.PanicOnErr(loginCmd.RegisterFlagCompletionFunc("api-token", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return cobra.AppendActiveHelp(nil, fmt.Sprintf("Please enter your api-token (you can instead set the variable %s)", config.EnvAPITokenKey)), cobra.ShellCompDirectiveNoFileComp + })) loginCmd.Flags().BoolVar(&staging, "staging", false, "use CSP staging issuer") loginCmd.Flags().StringVar(&endpointCACertPath, "endpoint-ca-certificate", "", "path to the endpoint public certificate") loginCmd.Flags().BoolVar(&skipTLSVerify, "insecure-skip-tls-verify", false, "skip endpoint's TLS certificate verification") - loginCmd.Flags().MarkHidden("stderr-only") //nolint - loginCmd.Flags().MarkHidden("force-csp") //nolint - loginCmd.Flags().MarkHidden("staging") //nolint + utils.PanicOnErr(loginCmd.Flags().MarkHidden("api-token")) + utils.PanicOnErr(loginCmd.Flags().MarkHidden("staging")) loginCmd.SetUsageFunc(cli.SubCmdUsageFunc) loginCmd.MarkFlagsMutuallyExclusive("endpoint-ca-certificate", "insecure-skip-tls-verify") - // TODO: Update the plugin-runtime library with the new format and use the library method - msg := fmt.Sprintf("this was done in the %q release, it will be removed following the deprecation policy (6 months). Use the %q command instead.\n", "v0.90.0", "context") - loginCmd.Deprecated = msg - loginCmd.Example = ` - # Login to TKG management cluster using endpoint - tanzu login --endpoint "https://login.example.com" --name mgmt-cluster - - # Login to TKG management cluster by using the provided CA Bundle for TLS verification: - tanzu login --endpoint https://k8s.example.com[:port] --endpoint-ca-certificate /path/to/ca/ca-cert - - # Login to TKG management cluster by explicit request to skip TLS verification, which is insecure: - tanzu login --endpoint https://k8s.example.com[:port] --insecure-skip-tls-verify - - # Login to TKG management cluster by using kubeconfig path and context for the management cluster - tanzu login --kubeconfig path/to/kubeconfig --context path/to/context --name mgmt-cluster - - # Login to TKG management cluster by using default kubeconfig path and context for the management cluster - tanzu login --context path/to/context --name mgmt-cluster - - # Login to an existing server - tanzu login --server mgmt-cluster - - Note: To create Mission Control (TMC) contexts, an API Key is required. The API Key value can - be provided with the environment variable TANZU_API_TOKEN or can be entered as input while - creating the context. - - [*] : Users have two options to login to TKG. They can choose the login endpoint option - by providing 'endpoint', or can choose to use the kubeconfig for the management cluster by - providing 'kubeconfig' and 'context'. If only '--context' is set and '--kubeconfig' is not, - the $KUBECONFIG env variable will be used and, if the $KUBECONFIG env is also not set, the - default kubeconfig file ($HOME/.kube/config) will be used.` + # Login to Tanzu + tanzu login + + # Login to Tanzu using non-default endpoint + tanzu login --endpoint "https://login.example.com" + + # Login to Tanzu by using the provided CA Bundle for TLS verification: + tanzu login --endpoint https://test.example.com[:port] --endpoint-ca-certificate /path/to/ca/ca-cert + + # Login to Tanzu by explicit request to skip TLS verification (this is insecure):: + tanzu login --endpoint https://test.example.com[:port] --insecure-skip-tls-verify + + Note: + To login to Tanzu an API Key is optional. If provided using the TANZU_API_TOKEN environment + variable, it will be used. Otherwise, the CLI will attempt to log in interactively to the user's default Cloud Services + organization. You can override or choose a custom organization by setting the TANZU_CLI_CLOUD_SERVICES_ORGANIZATION_ID + environment variable with the custom organization ID value. More information regarding organizations in Cloud Services + and how to obtain the organization ID can be found at + https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-CF9E9318-B811-48CF-8499-9419997DC1F8.html + Also, more information on logging into Tanzu Application SaaS platform and using + interactive login in terminal based hosts (without browser) can be found at + https://github.com/vmware-tanzu/tanzu-cli/blob/main/docs/quickstart/quickstart.md#logging-into-tanzu-application-platform +` } -func login(_ *cobra.Command, _ []string) (err error) { - cfg, err := config.GetClientConfig() - if err != nil { - return err - } - - newServerSelector := "+ new server" - var serverTarget *configtypes.Server //nolint:staticcheck // Deprecated - if name != "" { - serverTarget, err = createNewServer() - if err != nil { - return err - } - } else if server == "" { - serverTarget, err = getServerTarget(cfg, newServerSelector) - if err != nil { - return err - } - } else { - serverTarget, err = config.GetServer(server) //nolint:staticcheck // Deprecated - if err != nil { - return err - } - } - - if server == newServerSelector { - serverTarget, err = createNewServer() - if err != nil { - return err - } - } - - if serverTarget.Type == configtypes.GlobalServerType { //nolint:staticcheck // Deprecated - err = globalLoginUsingServer(serverTarget) - } else { - err = managementClusterLogin(serverTarget) - } +func login(cmd *cobra.Command, _ []string) (err error) { + // assign the loginEndpoint to context endpoint option variable + endpoint = loginEndpoint + // generate random context name to skip the prompts and later update the + // context name with organization name acquired after successful authentication + ctxName = uuid.New().String() + ctx, err := createContextUsingContextType(contextTanzu) if err != nil { return err } - // Sync all required plugins - if err = pluginmanager.SyncPlugins(); err != nil { - log.Warning("unable to automatically sync the plugins from target server. Please run 'tanzu plugin sync' command to sync plugins manually") - } - - return nil -} - -func getServerTarget(cfg *configtypes.ClientConfig, newServerSelector string) (*configtypes.Server, error) { //nolint:staticcheck // Deprecated - promptOpts := getPromptOpts() - servers := map[string]*configtypes.Server{} //nolint:staticcheck // Deprecated - for _, server := range cfg.KnownServers { //nolint:staticcheck // Deprecated - if server.Type != configtypes.GlobalServerType && server.Type != configtypes.ManagementClusterServerType { //nolint:staticcheck // Deprecated - continue - } - ep, err := config.EndpointFromServer(server) //nolint:staticcheck // Deprecated - if err != nil { - return nil, err - } - - s := rpad(server.Name, 20) - s = fmt.Sprintf("%s(%s)", s, ep) - servers[s] = server - } - if endpoint == "" { - endpoint, _ = os.LookupEnv(config.EnvEndpointKey) - } - // If there are no existing servers - if len(servers) == 0 { - return createNewServer() - } - serverKeys := getKeysFromServerMap(servers) - serverKeys = append(serverKeys, newServerSelector) - servers[newServerSelector] = &configtypes.Server{} //nolint:staticcheck // Deprecated - err := component.Prompt( - &component.PromptConfig{ - Message: "Select a server", - Options: serverKeys, - Default: serverKeys[0], - }, - &server, - promptOpts..., - ) + err = globalTanzuLogin(ctx, prepareTanzuContextName) if err != nil { - return nil, err - } - return servers[server], nil -} - -func getKeysFromServerMap(m map[string]*configtypes.Server) []string { //nolint:staticcheck // Deprecated - keys := make([]string, 0, len(m)) - for key := range m { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -func isGlobalServer(endpoint string) bool { - if strings.Contains(endpoint, knownGlobalHost) { - return true - } - if forceCSP { - return true - } - return false -} - -func createNewServer() (server *configtypes.Server, err error) { //nolint:staticcheck // Deprecated - // user provided command line options to create a server using kubeconfig[optional] and context - if kubeContext != "" { - return createServerWithKubeconfig() - } - // user provided command line options to create a server using endpoint - if endpoint != "" { - return createServerWithEndpoint() + return err } - promptOpts := getPromptOpts() - var loginType string - - err = component.Prompt( - &component.PromptConfig{ - Message: "Select login type", - Options: []string{"Server endpoint", "Local kubeconfig"}, - Default: "Server endpoint", - }, - &loginType, - promptOpts..., - ) - if err != nil { - return server, err + // if user performs re-login having an existing context with active resource set to project/space/clustergroup + // update the kubeconfig because "globalTanzuLogin" updates the kubeconfig to point to organization only, + if err := updateKubeConfigForContext(ctx); err != nil { + return nil } - if loginType == "Server endpoint" { - return createServerWithEndpoint() - } + // Sync all required plugins + _ = syncContextPlugins(cmd, ctx.ContextType, ctxName, true) - return createServerWithKubeconfig() + return nil } -func createServerWithKubeconfig() (server *configtypes.Server, err error) { //nolint:staticcheck // Deprecated - promptOpts := getPromptOpts() - if kubeConfig == "" && kubeContext == "" { - err = component.Prompt( - &component.PromptConfig{ - Message: "Enter path to kubeconfig (if any)", - }, - &kubeConfig, - promptOpts..., - ) - if err != nil { - return server, err - } - } - kubeConfig = strings.TrimSpace(kubeConfig) - if kubeConfig == "" { - kubeConfig = kubecfg.GetDefaultKubeConfigFile() - } +func updateKubeConfigForContext(c *configtypes.Context) error { + projNameStr := getString(c.AdditionalMetadata[config.ProjectNameKey]) + projIDStr := getString(c.AdditionalMetadata[config.ProjectIDKey]) + spaceNameStr := getString(c.AdditionalMetadata[config.SpaceNameKey]) + clusterGroupNameNameStr := getString(c.AdditionalMetadata[config.ClusterGroupNameKey]) - if kubeConfig != "" && kubeContext == "" { - err = component.Prompt( - &component.PromptConfig{ - Message: "Enter kube context to use", - }, - &kubeContext, - promptOpts..., - ) - if err != nil { - return server, err - } - } - kubeContext = strings.TrimSpace(kubeContext) - if name == "" { - err = component.Prompt( - &component.PromptConfig{ - Message: "Give the server a name", - }, - &name, - promptOpts..., - ) - if err != nil { - return server, err - } - } - name = strings.TrimSpace(name) - nameExists, err := config.ServerExists(name) //nolint:staticcheck // Deprecated - if err != nil { - return server, err - } - if nameExists { - err := fmt.Errorf("server %q already exists", name) - return server, err - } - - endpointType := configtypes.ManagementClusterServerType //nolint:staticcheck // Deprecated - - server = &configtypes.Server{ //nolint:staticcheck // Deprecated - Name: name, - Type: endpointType, - ManagementClusterOpts: &configtypes.ManagementClusterServer{ //nolint:staticcheck // Deprecated - Path: kubeConfig, - Context: kubeContext, - Endpoint: endpoint}, - } - return server, err + projectVal := getProjectValueForKubeconfig(projNameStr, projIDStr) + return updateTanzuContextKubeconfig(c, projectVal, spaceNameStr, clusterGroupNameNameStr) } -func createServerWithEndpoint() (server *configtypes.Server, err error) { //nolint:staticcheck // Deprecated - promptOpts := getPromptOpts() - if endpoint == "" { - err = component.Prompt( - &component.PromptConfig{ - Message: "Enter server endpoint", - }, - &endpoint, - promptOpts..., - ) - if err != nil { - return server, err - } - } - endpoint = strings.TrimSpace(endpoint) - if name == "" { - err = component.Prompt( - &component.PromptConfig{ - Message: "Give the server a name", - }, - &name, - promptOpts..., - ) - if err != nil { - return server, err - } - } - name = strings.TrimSpace(name) - nameExists, err := config.ServerExists(name) //nolint:staticcheck // Deprecated - if err != nil { - return server, err - } - if nameExists { - err := fmt.Errorf("server %q already exists", name) - return server, err - } - if isGlobalServer(endpoint) { - server = &configtypes.Server{ //nolint:staticcheck // Deprecated - Name: name, - Type: configtypes.GlobalServerType, //nolint:staticcheck // Deprecated - GlobalOpts: &configtypes.GlobalServer{Endpoint: sanitizeEndpoint(endpoint)}, - } - } else { - tkf := NewTKGKubeconfigFetcher(endpoint, endpointCACertPath, skipTLSVerify) - kubeConfig, kubeContext, err = tkf.GetPinnipedKubeconfig() - if err != nil { - return server, err - } - - server = &configtypes.Server{ //nolint:staticcheck // Deprecated - Name: name, - Type: configtypes.ManagementClusterServerType, //nolint:staticcheck // Deprecated - ManagementClusterOpts: &configtypes.ManagementClusterServer{ //nolint:staticcheck // Deprecated - Path: kubeConfig, - Context: kubeContext, - Endpoint: endpoint}, - } +func getString(data interface{}) string { + if _, ok := data.(string); !ok { + return "" } - return server, err + return data.(string) } -func globalLoginUsingServer(s *configtypes.Server) (err error) { //nolint:staticcheck // Deprecated - a := configtypes.GlobalServerAuth{} - apiTokenValue, apiTokenExists := os.LookupEnv(config.EnvAPITokenKey) - - issuer := csp.ProdIssuer - if staging { - issuer = csp.StgIssuer +// prepareTanzuContextName returns the context name given organization name,endpoint and staging details +// pre-req orgName and endpoint is non-empty string +func prepareTanzuContextName(orgName, endpoint string, isStaging bool) string { + contextName := strings.Replace(orgName, " ", "_", -1) + if isStaging { + contextName += "-staging" } - if apiTokenExists { - log.Info("API token env var is set") - } else { - fmt.Fprintln(os.Stderr) - log.Info("The API key can be provided by setting the TANZU_API_TOKEN environment variable") - apiTokenValue, err = promptAPIToken("TMC") - if err != nil { - return err - } - } - token, err := csp.GetAccessTokenFromAPIToken(apiTokenValue, issuer) - if err != nil { - return err - } - claims, err := csp.ParseToken(&oauth2.Token{AccessToken: token.AccessToken}) - if err != nil { - return err - } - - a.Issuer = issuer - - a.UserName = claims.Username - a.Permissions = claims.Permissions - a.AccessToken = token.AccessToken - a.IDToken = token.IDToken - a.RefreshToken = apiTokenValue - a.Type = "api-token" - - expiresAt := time.Now().Local().Add(time.Second * time.Duration(token.ExpiresIn)) - a.Expiration = expiresAt - if s != nil && s.GlobalOpts != nil { - s.GlobalOpts.Auth = a + if endpoint != defaultTanzuEndpoint { + // append just 8 chars of sha to the context name + contextName += fmt.Sprintf("-%s", hashString(endpoint)[:8]) } - - err = config.PutServer(s, true) //nolint:staticcheck // Deprecated - if err != nil { - return err - } - - fmt.Println() - log.Success("successfully logged into global control plane") - return nil + return contextName } -func managementClusterLogin(s *configtypes.Server) error { //nolint:staticcheck // Deprecated - if s != nil && s.ManagementClusterOpts != nil && s.ManagementClusterOpts.Path != "" && s.ManagementClusterOpts.Context != "" { - _, err := tkgauth.GetServerKubernetesVersion(s.ManagementClusterOpts.Path, s.ManagementClusterOpts.Context) - if err != nil { - err := fmt.Errorf("failed to login to the management cluster %s, %v", s.Name, err) - log.Error(err, "") - return err - } - err = config.PutServer(s, true) //nolint:staticcheck // Deprecated - if err != nil { - return err - } - - log.Successf("successfully logged in to management cluster using the kubeconfig %s", s.Name) - return nil - } - - return fmt.Errorf("not yet implemented") +func hashString(str string) string { + h := sha256.New() + h.Write([]byte(str)) + return hex.EncodeToString(h.Sum(nil)) } diff --git a/pkg/command/login_test.go b/pkg/command/login_test.go index 3dac1f6e8..b5d3bfcb0 100644 --- a/pkg/command/login_test.go +++ b/pkg/command/login_test.go @@ -3,152 +3,50 @@ package command import ( - "os" - "path/filepath" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/otiai10/copy" - "k8s.io/client-go/tools/clientcmd" - - "github.com/vmware-tanzu/tanzu-cli/pkg/constants" - configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" + "testing" ) -var _ = Describe("create new server", func() { - const ( - existingContext = "test-mc" - testKubeContext = "test-k8s-context" - testKubeConfigPath = "/fake/path/kubeconfig" - testServerName = "fake-server-name" - fakeTMCEndpoint = "https://cloud.vmware.com/auth" - ) - Describe("create server with kubeconfig", func() { - var ( - tkgConfigFile *os.File - tkgConfigFileNG *os.File - err error - svr *configtypes.Server - ) - - BeforeEach(func() { - tkgConfigFile, err = os.CreateTemp("", "config") - Expect(err).To(BeNil()) - err = copy.Copy(filepath.Join("..", "fakes", "config", "tanzu_config.yaml"), tkgConfigFile.Name()) - Expect(err).To(BeNil(), "Error while copying tanzu config file for testing") - os.Setenv("TANZU_CONFIG", tkgConfigFile.Name()) - - tkgConfigFileNG, err = os.CreateTemp("", "config_ng") - Expect(err).To(BeNil()) - os.Setenv("TANZU_CONFIG_NEXT_GEN", tkgConfigFileNG.Name()) - err = copy.Copy(filepath.Join("..", "fakes", "config", "tanzu_config_ng.yaml"), tkgConfigFileNG.Name()) - Expect(err).To(BeNil(), "Error while coping tanzu config file for testing") - - err = os.Setenv(constants.EULAPromptAnswer, "yes") - Expect(err).To(BeNil()) - }) - AfterEach(func() { - os.Unsetenv("TANZU_CONFIG") - os.Unsetenv("TANZU_CONFIG_NEXT_GEN") - os.RemoveAll(tkgConfigFile.Name()) - os.RemoveAll(tkgConfigFileNG.Name()) - os.Unsetenv(constants.EULAPromptAnswer) - resetLoginCommandFlags() - }) - Context("with only kubecontext provided", func() { - It("should create server with given kubecontext and default kubeconfig path", func() { - kubeContext = testKubeContext - name = testServerName - svr, err = createNewServer() - Expect(err).To(BeNil()) - Expect(svr.Name).To(ContainSubstring(name)) - Expect(svr.Type).To(BeEquivalentTo(configtypes.ManagementClusterServerType)) - Expect(svr.ManagementClusterOpts.Context).To(ContainSubstring("test-k8s-context")) - Expect(svr.ManagementClusterOpts.Path).To(ContainSubstring(clientcmd.RecommendedHomeFile)) - }) - }) - Context("with both kubeconfig and kubecontext provided", func() { - It("should create server with given kubecontext and kubeconfig path", func() { - kubeContext = testKubeContext - kubeConfig = testKubeConfigPath - name = testServerName - svr, err = createNewServer() - Expect(err).To(BeNil()) - Expect(svr.Name).To(ContainSubstring(name)) - Expect(svr.Type).To(BeEquivalentTo(configtypes.ManagementClusterServerType)) - Expect(svr.ManagementClusterOpts.Context).To(ContainSubstring("test-k8s-context")) - Expect(svr.ManagementClusterOpts.Path).To(ContainSubstring(kubeConfig)) - }) - }) - Context("server name already exists", func() { - It("should return error", func() { - kubeContext = testKubeContext - kubeConfig = testKubeConfigPath - name = existingContext - svr, err = createNewServer() - Expect(err).ToNot(BeNil()) - Expect(err.Error()).To(ContainSubstring(`server "test-mc" already exists`)) - }) - }) - Describe("create server with tmc server endpoint", func() { - var ( - tkgConfigFile *os.File - tkgConfigFileNG *os.File - err error - svr *configtypes.Server - ) - - BeforeEach(func() { - tkgConfigFile, err = os.CreateTemp("", "config") - Expect(err).To(BeNil()) - err = copy.Copy(filepath.Join("..", "fakes", "config", "tanzu_config.yaml"), tkgConfigFile.Name()) - Expect(err).To(BeNil(), "Error while copying tanzu config file for testing") - os.Setenv("TANZU_CONFIG", tkgConfigFile.Name()) - - tkgConfigFileNG, err = os.CreateTemp("", "config_ng") - Expect(err).To(BeNil()) - os.Setenv("TANZU_CONFIG_NEXT_GEN", tkgConfigFileNG.Name()) - err = copy.Copy(filepath.Join("..", "fakes", "config", "tanzu_config_ng.yaml"), tkgConfigFileNG.Name()) - Expect(err).To(BeNil(), "Error while coping tanzu config file for testing") - }) - AfterEach(func() { - os.Unsetenv("TANZU_CONFIG") - os.Unsetenv("TANZU_CONFIG_NEXT_GEN") - os.RemoveAll(tkgConfigFile.Name()) - os.RemoveAll(tkgConfigFileNG.Name()) - resetLoginCommandFlags() - }) - Context("with only endpoint and context name provided", func() { - It("should create server with given endpoint and context name", func() { - endpoint = fakeTMCEndpoint - name = "fake-server-name" - svr, err = createNewServer() - Expect(err).To(BeNil()) - Expect(svr.Name).To(ContainSubstring(name)) - Expect(svr.Type).To(BeEquivalentTo(configtypes.GlobalServerType)) - Expect(svr.GlobalOpts.Endpoint).To(ContainSubstring(endpoint)) - }) - }) - Context("server name already exists", func() { - It("should return error", func() { - endpoint = fakeTMCEndpoint - name = existingContext - svr, err = createNewServer() - Expect(err).ToNot(BeNil()) - Expect(err.Error()).To(ContainSubstring(`server "test-mc" already exists`)) - }) - }) - }) - }) -}) - -func resetLoginCommandFlags() { - name = "" - endpoint = "" - apiToken = "" - kubeConfig = "" - kubeContext = "" - server = "" - skipTLSVerify = false - endpointCACertPath = "" +func TestPrepareTanzuContextName(t *testing.T) { + testCases := []struct { + orgName string + endpoint string + isStaging bool + expected string + }{ + // Test case for normal input with no staging environment and default endpoint. + { + orgName: "MyOrg", + endpoint: defaultTanzuEndpoint, + isStaging: false, + expected: "MyOrg", + }, + // Test case for normal input with staging environment and default endpoint. + { + orgName: "MyOrg", + endpoint: defaultTanzuEndpoint, + isStaging: true, + expected: "MyOrg-staging", + }, + // Test case for normal input with no staging environment and custom endpoint. + { + orgName: "MyOrg", + endpoint: "https://custom-endpoint.com", + isStaging: false, + expected: "MyOrg-86fd8133", + }, + // Test case for normal input with staging environment and custom endpoint. + { + orgName: "MyOrg", + endpoint: "https://custom-endpoint.com", + isStaging: true, + expected: "MyOrg-staging-86fd8133", + }, + } + + for _, tc := range testCases { + actual := prepareTanzuContextName(tc.orgName, tc.endpoint, tc.isStaging) + if actual != tc.expected { + t.Errorf("orgName: %s, endpoint: %s, isStaging: %t - expected: %s, got: %s", tc.orgName, tc.endpoint, tc.isStaging, tc.expected, actual) + } + } }