Skip to content

Commit

Permalink
Add "tanzu context get-token" command to fetch a valid CSP token for …
Browse files Browse the repository at this point in the history
…the given context

- Add a hidden command "tanzu context get-token" to fetch a valid CSP token for the given context. This command would be used in the kubeconfig generated for UCP resource to fetch/refresh the access-token dynamically.
- Updated the kubeconfig generation logic to include the exec plugin to fetch the access token dynamically

Signed-off-by: Prem Kumar Kalle <[email protected]>
  • Loading branch information
prkalle committed Sep 29, 2023
1 parent f658734 commit 4f9014f
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 6 deletions.
2 changes: 1 addition & 1 deletion pkg/auth/csp/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func GetToken(g *configapi.GlobalServerAuth) (*oauth2.Token, error) {
}

// TODO (pbarker): support more issuers.
token, err := GetAccessTokenFromAPIToken(g.RefreshToken, ProdIssuer)
token, err := GetAccessTokenFromAPIToken(g.RefreshToken, g.Issuer)
if err != nil {
return nil, err
}
Expand Down
18 changes: 16 additions & 2 deletions pkg/auth/ucp/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/pkg/errors"
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"

kubeutils "github.com/vmware-tanzu/tanzu-cli/pkg/auth/utils/kubeconfig"
Expand All @@ -37,7 +38,7 @@ func GetUCPKubeconfig(c *configtypes.Context, endpoint, orgID, endpointCACertPat
contextName := kubeconfigContextName(c.Name)
clusterName := kubeconfigClusterName(c.Name)
username := kubeconfigUserName(c.Name)

execConfig := getExecConfig(c)
config := &clientcmdapi.Config{
Kind: "Config",
APIVersion: clientcmdapi.SchemeGroupVersion.Version,
Expand All @@ -46,7 +47,7 @@ func GetUCPKubeconfig(c *configtypes.Context, endpoint, orgID, endpointCACertPat
InsecureSkipTLSVerify: skipTLSVerify,
Server: clusterAPIServerURL,
}},
AuthInfos: map[string]*clientcmdapi.AuthInfo{username: {Token: c.GlobalOpts.Auth.AccessToken}},
AuthInfos: map[string]*clientcmdapi.AuthInfo{username: {Exec: execConfig}},
Contexts: map[string]*clientcmdapi.Context{contextName: {Cluster: clusterName, AuthInfo: username}},
CurrentContext: contextName,
}
Expand Down Expand Up @@ -75,3 +76,16 @@ func kubeconfigClusterName(ucpContextName string) string {
func kubeconfigUserName(ucpContextName string) string {
return "tanzu-cli-" + ucpContextName + "-user"
}

func getExecConfig(c *configtypes.Context) *clientcmdapi.ExecConfig {
execConfig := &clientcmdapi.ExecConfig{
APIVersion: clientauthenticationv1.SchemeGroupVersion.String(),
Args: []string{},
Env: []clientcmdapi.ExecEnvVar{},
InteractiveMode: clientcmdapi.NeverExecInteractiveMode,
}

execConfig.Command = "tanzu"
execConfig.Args = append([]string{"context", "get-token"}, c.Name)
return execConfig
}
4 changes: 2 additions & 2 deletions pkg/auth/ucp/kubeconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ var _ = Describe("Unit tests for ucp auth", func() {
Expect(config.Contexts[kubeContext].AuthInfo).To(Equal(kubeconfigUserName(ucpContext.Name)))
Expect(gotClusterName).To(Equal(kubeconfigClusterName(ucpContext.Name)))
Expect(len(cluster.CertificateAuthorityData)).ToNot(Equal(0))
Expect(user.Token).To(Equal(ucpContext.GlobalOpts.Auth.AccessToken))
Expect(user.Exec).To(Equal(getExecConfig(ucpContext)))
})
})
Context("When endpointCACertPath is not provided and skipTLSVerify is set to true", func() {
Expand All @@ -115,7 +115,7 @@ var _ = Describe("Unit tests for ucp auth", func() {
Expect(gotClusterName).To(Equal("tanzu-cli-" + ucpContext.Name + "/current"))
Expect(len(cluster.CertificateAuthorityData)).To(Equal(0))
Expect(cluster.InsecureSkipTLSVerify).To(Equal(true))
Expect(user.Token).To(Equal(ucpContext.GlobalOpts.Auth.AccessToken))
Expect(user.Exec).To(Equal(getExecConfig(ucpContext)))
})
})

Expand Down
52 changes: 52 additions & 0 deletions pkg/command/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package command

import (
"crypto/tls"
"encoding/json"
"fmt"
"io"

"net/http"
"net/url"
"os"
Expand All @@ -20,6 +22,8 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
"k8s.io/client-go/tools/clientcmd"

"github.com/vmware-tanzu/tanzu-plugin-runtime/component"
Expand Down Expand Up @@ -82,6 +86,7 @@ func init() {
deleteCtxCmd,
useCtxCmd,
unsetCtxCmd,
getCtxTokenCmd,
)

initCreateCtxCmd()
Expand Down Expand Up @@ -1026,3 +1031,50 @@ func displayContextListOutputSplitViewTarget(cfg *configtypes.ClientConfig, writ
outputWriterTMCTarget.Render()
}
}

var getCtxTokenCmd = &cobra.Command{
Use: "get-token CONTEXT_NAME",
Short: "Get the valid CSP token for the given UCP context.",
Args: cobra.ExactArgs(1),
Hidden: true,
RunE: getToken,
}

func getToken(cmd *cobra.Command, args []string) error {
name := args[0]
ctx, err := config.GetContext(name)
if err != nil {
return err
}
if ctx.Target != configtypes.TargetUCP {
return errors.Errorf("context %q is not of type UCP", name)
}
if csp.IsExpired(ctx.GlobalOpts.Auth.Expiration) {
_, err := csp.GetToken(&ctx.GlobalOpts.Auth)
if err != nil {
return errors.Wrap(err, "failed to refresh the token")
}
if err = config.SetContext(ctx, false); err != nil {
return errors.Wrap(err, "failed updating the context after token refresh")
}
}
token := ctx.GlobalOpts.Auth.AccessToken
expTime := ctx.GlobalOpts.Auth.Expiration

return printTokenToStdout(cmd, token, expTime)
}

func printTokenToStdout(cmd *cobra.Command, token string, expTime time.Time) error {
et := metav1.NewTime(expTime).Rfc3339Copy()
cred := clientauthv1.ExecCredential{
TypeMeta: metav1.TypeMeta{
Kind: "ExecCredential",
APIVersion: "client.authentication.k8s.io/v1",
},
Status: &clientauthv1.ExecCredentialStatus{
Token: token,
ExpirationTimestamp: &et,
},
}
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
}
78 changes: 77 additions & 1 deletion pkg/command/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import (
"bytes"
"encoding/json"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
"os"
"path/filepath"
"testing"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -255,6 +258,79 @@ clusterOpts:
os.Unsetenv(constants.SkipUpdateKubeconfigOnContextUse)
})
})
Describe("tanzu context get-token", func() {
const (
fakeContextName = "fake-context"
fakeAccessToken = "fake-access-token"
fakeEndpoint = "fake.ucp.cloud.vmware.com"
fakeIssuer = "https://fake.issuer.come/auth"
)
var err error
cmd := &cobra.Command{}
ucpContext := &configtypes.Context{}

BeforeEach(func() {
cmd.SetOut(&buf)

ucpContext = &configtypes.Context{
Name: fakeContextName,
Target: configtypes.TargetUCP,
GlobalOpts: &configtypes.GlobalServer{
Endpoint: fakeEndpoint,
Auth: configtypes.GlobalServerAuth{
AccessToken: fakeAccessToken,
Issuer: fakeIssuer,
},
},
}
})
AfterEach(func() {
resetContextCommandFlags()
buf.Reset()
})
It("should return error if the context to be used doesn't exist", func() {
err = getToken(cmd, []string{"non-existing-context"})
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("context non-existing-context not found"))

})
It("should return error if the context type is not UCP", func() {
ucpContext.Target = configtypes.TargetK8s
err = config.SetContext(ucpContext, false)
Expect(err).To(BeNil())

err = getToken(cmd, []string{fakeContextName})
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring(`context "fake-context" is not of type UCP`))

})
It("should return error if the access token refresh fails", func() {
ucpContext.GlobalOpts.Auth.Expiration = time.Now().Add(-time.Hour)

err = config.SetContext(ucpContext, false)
Expect(err).To(BeNil())
err = getToken(cmd, []string{fakeContextName})
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("failed to refresh the token"))
})
It("should print the exec credentials if the access token is valid(not expired)", func() {
ucpContext.GlobalOpts.Auth.Expiration = time.Now().Add(time.Hour)

err = config.SetContext(ucpContext, false)
Expect(err).To(BeNil())
err = getToken(cmd, []string{fakeContextName})
Expect(err).To(BeNil())

execCredential := &clientauthv1.ExecCredential{}
err = json.NewDecoder(&buf).Decode(execCredential)
Expect(err).To(BeNil())
Expect(execCredential.Kind).To(Equal("ExecCredential"))
Expect(execCredential.APIVersion).To(Equal("client.authentication.k8s.io/v1"))
Expect(execCredential.Status.Token).To(Equal(fakeAccessToken))
expectedTime := metav1.NewTime(ucpContext.GlobalOpts.Auth.Expiration).Rfc3339Copy()
Expect(execCredential.Status.ExpirationTimestamp.Equal(&expectedTime)).To(BeTrue())
})
})

Describe("tanzu context unset", func() {
cmd := &cobra.Command{}
Expand Down Expand Up @@ -483,7 +559,7 @@ var _ = Describe("create new context", func() {
tlsServer.Close()
})

Describe("create context with self-managed tmc endpoint", func() {
Describe("create context with ucp endpoint", func() {
var (
tkgConfigFile *os.File
tkgConfigFileNG *os.File
Expand Down

0 comments on commit 4f9014f

Please sign in to comment.