From 8838b49777a99e9887ad62a417cbb41b2947b1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20D=C3=ADaz?= <49416765+AgustinRamiroDiaz@users.noreply.github.com> Date: Tue, 28 Mar 2023 19:34:14 +0200 Subject: [PATCH] Okteto kubeconfig will use kubetoken client auth if available on okteto server (#3409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fill user authinfo with exec * Check for kubetoken capability on server * Add context for kubetoken to user.Exec * Use oktetoURL and namespace in exec args * Update golang-ci * add os.Environ to cmd Signed-off-by: Agustín Díaz --- .circleci/config.yml | 4 +- Makefile | 5 +- cmd/context/create.go | 24 +++++- cmd/context/create_test.go | 3 + cmd/namespace/create_test.go | 3 + integration/actions/apply_test.go | 1 + integration/commands/preview.go | 1 + integration/commands/push.go | 1 + integration/commands/stacks.go | 2 + pkg/okteto/client.go | 6 +- pkg/okteto/context.go | 28 ++++++- pkg/okteto/context_test.go | 130 ++++++++++++++++++++++++++++++ pkg/types/credential.go | 2 +- 13 files changed, 197 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e0bc540961d..66752590b4a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -52,7 +52,7 @@ commands: executors: golang-ci: docker: - - image: okteto/golang-ci:1.18.0 + - image: okteto/golang-ci:2.2.2 jobs: build-binaries: @@ -139,11 +139,11 @@ jobs: chmod +x /usr/local/bin/kubectl cp $(pwd)/artifacts/bin/okteto-Linux-x86_64 /usr/local/bin/okteto /usr/local/bin/okteto login --token ${API_STAGING_TOKEN} + - integration-deprecated - integration-deploy - integration-up - integration-actions - integration-okteto - - integration-deprecated - integration-build - save_cache: key: v4-pkg-cache-{{ checksum "go.sum" }} diff --git a/Makefile b/Makefile index 3516344c50cb..9e92cf264907 100644 --- a/Makefile +++ b/Makefile @@ -65,8 +65,9 @@ integration-up: .PHONY: integration-deprecated integration-deprecated: - go test github.com/okteto/okteto/integration/deprecated/push -tags="integration" --count=1 -v -timeout 15m && go test github.com/okteto/okteto/integration/deprecated/stack -tags="integration" --count=1 -v -timeout 15m - + go test github.com/okteto/okteto/integration/deprecated/stack -tags="integration" --count=1 -v -timeout 15m && \ + go test github.com/okteto/okteto/integration/deprecated/push -tags="integration" --count=1 -v -timeout 15m + .PHONY: build build: $(BUILDCOMMAND) -o ${BINDIR}/okteto diff --git a/cmd/context/create.go b/cmd/context/create.go index c6daea89c0d4..4cb4c89f1e61 100644 --- a/cmd/context/create.go +++ b/cmd/context/create.go @@ -17,6 +17,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "net/url" "os" "strings" @@ -42,7 +43,8 @@ type ContextCommand struct { LoginController login.LoginInterface OktetoClientProvider types.OktetoClientProvider - OktetoContextWriter okteto.ContextConfigWriterInterface + OktetoContextWriter okteto.ContextConfigWriterInterface + IsOktetoKubeTokenPresent func(url string) (bool, error) } // NewContextCommand creates a new ContextCommand @@ -52,6 +54,21 @@ func NewContextCommand() *ContextCommand { LoginController: login.NewLoginController(), OktetoClientProvider: okteto.NewOktetoClientProvider(), OktetoContextWriter: okteto.NewContextConfigWriter(), + IsOktetoKubeTokenPresent: func(oktetoURL string) (bool, error) { + // this is a hack to check if the server was upgraded to a version that supports the /auth/kubetoken/{namespace} endpoint + // if the endpoint is not present, the server will return a 401 unauthorized error + kubeTokenURL, err := okteto.ParseOktetoURLWithPath(oktetoURL, "auth/kubetoken/default") + if err != nil { + return false, fmt.Errorf("failed to parse kubetoken url: %w", err) + } + + resp, err := http.Get(kubeTokenURL) + if err != nil { + return false, fmt.Errorf("failed to get kubetoken url: %w", err) + } + + return resp.StatusCode == http.StatusUnauthorized, nil + }, } } @@ -264,7 +281,10 @@ func (c *ContextCommand) initOktetoContext(ctx context.Context, ctxOptions *Cont if cfg == nil { cfg = kubeconfig.Create() } - okteto.AddOktetoCredentialsToCfg(cfg, &userContext.Credentials, ctxOptions.Namespace, userContext.User.ID, okteto.Context().Name) + err = okteto.AddOktetoCredentialsToCfg(cfg, &userContext.Credentials, ctxOptions.Namespace, userContext.User.ID, okteto.Context().Name, c.IsOktetoKubeTokenPresent) + if err != nil { + return err + } okteto.Context().Cfg = cfg okteto.Context().IsOkteto = true okteto.Context().IsInsecure = okteto.IsInsecureSkipTLSVerifyPolicy() diff --git a/cmd/context/create_test.go b/cmd/context/create_test.go index 0f0e9179ac44..56c80ea98838 100644 --- a/cmd/context/create_test.go +++ b/cmd/context/create_test.go @@ -38,6 +38,9 @@ func newFakeContextCommand(c *client.FakeOktetoClient, user *types.User, fakeObj LoginController: test.NewFakeLoginController(user, nil), OktetoClientProvider: client.NewFakeOktetoClientProvider(c), OktetoContextWriter: test.NewFakeOktetoContextWriter(), + IsOktetoKubeTokenPresent: func(url string) (bool, error) { + return false, nil + }, } } diff --git a/cmd/namespace/create_test.go b/cmd/namespace/create_test.go index 4cc172c7dea3..ac2faae3350d 100644 --- a/cmd/namespace/create_test.go +++ b/cmd/namespace/create_test.go @@ -32,6 +32,9 @@ func newFakeContextCommand(c *client.FakeOktetoClient, user *types.User) *contex K8sClientProvider: test.NewFakeK8sProvider(nil), LoginController: test.NewFakeLoginController(user, nil), OktetoContextWriter: test.NewFakeOktetoContextWriter(), + IsOktetoKubeTokenPresent: func(url string) (bool, error) { + return false, nil + }, } } diff --git a/integration/actions/apply_test.go b/integration/actions/apply_test.go index 155a7bb9aa7a..41307828c013 100644 --- a/integration/actions/apply_test.go +++ b/integration/actions/apply_test.go @@ -118,6 +118,7 @@ func executeApply(namespace string) error { if _, err := os.Stat(kubepath); err != nil { log.Printf("could not get kubepath: %s", err) } + cmd.Env = os.Environ() cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", kubepath)) o, err := cmd.CombinedOutput() if err != nil { diff --git a/integration/commands/preview.go b/integration/commands/preview.go index 445816f4cb46..4ef89eb48984 100644 --- a/integration/commands/preview.go +++ b/integration/commands/preview.go @@ -90,6 +90,7 @@ func RunOktetoDeployPreview(oktetoPath string, deployOptions *DeployPreviewOptio func RunOktetoPreviewDestroy(oktetoPath string, destroyOptions *DestroyPreviewOptions) error { log.Printf("okteto destroy %s", oktetoPath) cmd := exec.Command(oktetoPath, "preview", "destroy", destroyOptions.Namespace) + cmd.Env = os.Environ() if destroyOptions.Workdir != "" { cmd.Dir = destroyOptions.Workdir } diff --git a/integration/commands/push.go b/integration/commands/push.go index 56c6233d62da..4bb357b294aa 100644 --- a/integration/commands/push.go +++ b/integration/commands/push.go @@ -26,6 +26,7 @@ import ( // RunOktetoPush runs an okteto push command func RunOktetoPush(oktetoPath, workdir string) error { cmd := exec.Command(oktetoPath, "push") + cmd.Env = os.Environ() if workdir != "" { cmd.Dir = workdir cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", constants.OktetoHomeEnvVar, workdir)) diff --git a/integration/commands/stacks.go b/integration/commands/stacks.go index 8a3a5b810e6b..91d42ff5c412 100644 --- a/integration/commands/stacks.go +++ b/integration/commands/stacks.go @@ -43,6 +43,7 @@ type StackDestroyOptions struct { // RunOktetoStackDeploy runs an okteto deploy command func RunOktetoStackDeploy(oktetoPath string, deployOptions *StackDeployOptions) error { cmd := exec.Command(oktetoPath, "stack", "deploy") + cmd.Env = os.Environ() if deployOptions.Workdir != "" { cmd.Dir = deployOptions.Workdir } @@ -77,6 +78,7 @@ func RunOktetoStackDeploy(oktetoPath string, deployOptions *StackDeployOptions) // RunOktetoStackDestroy runs an okteto deploy command func RunOktetoStackDestroy(oktetoPath string, deployOptions *StackDestroyOptions) error { cmd := exec.Command(oktetoPath, "stack", "destroy") + cmd.Env = os.Environ() if deployOptions.Workdir != "" { cmd.Dir = deployOptions.Workdir } diff --git a/pkg/okteto/client.go b/pkg/okteto/client.go index 7da3bc2d73bf..f569b2934e22 100644 --- a/pkg/okteto/client.go +++ b/pkg/okteto/client.go @@ -81,7 +81,7 @@ func newOktetoHttpClient(contextName, token, oktetoUrlPath string) (*http.Client if token == "" { return nil, "", fmt.Errorf(oktetoErrors.ErrNotLogged, contextName) } - u, err := parseOktetoURLWithPath(contextName, oktetoUrlPath) + u, err := ParseOktetoURLWithPath(contextName, oktetoUrlPath) if err != nil { return nil, "", err } @@ -177,10 +177,10 @@ func newOktetoClientFromGraphqlClient(url string, httpClient *http.Client) (*Okt } func parseOktetoURL(u string) (string, error) { - return parseOktetoURLWithPath(u, "graphql") + return ParseOktetoURLWithPath(u, "graphql") } -func parseOktetoURLWithPath(u, path string) (string, error) { +func ParseOktetoURLWithPath(u, path string) (string, error) { if u == "" { return "", errURLNotSet } diff --git a/pkg/okteto/context.go b/pkg/okteto/context.go index f1e5433b244b..d97bfaa15491 100644 --- a/pkg/okteto/context.go +++ b/pkg/okteto/context.go @@ -338,11 +338,16 @@ func (*ContextConfigWriter) Write() error { return nil } -func AddOktetoCredentialsToCfg(cfg *clientcmdapi.Config, cred *types.Credential, namespace, userName, oktetoURL string) { +func AddOktetoCredentialsToCfg(cfg *clientcmdapi.Config, cred *types.Credential, namespace, userName, oktetoURL string, hasKubeTokenCapabily func(oktetoURL string) (bool, error)) error { // If the context is being initialized within the execution of `okteto deploy` deploy command it should not // write the Okteto credentials into the kubeconfig. It would overwrite the proxy settings if os.Getenv(constants.OktetoSkipConfigCredentialsUpdate) == "true" { - return + return nil + } + + hasKubeToken, err := hasKubeTokenCapabily(oktetoURL) + if err != nil { + return err } clusterName := UrlToKubernetesContext(oktetoURL) @@ -361,7 +366,23 @@ func AddOktetoCredentialsToCfg(cfg *clientcmdapi.Config, cred *types.Credential, if !ok { user = clientcmdapi.NewAuthInfo() } - user.Token = cred.Token + + if hasKubeToken { + user.Token = "" + user.Exec = &clientcmdapi.ExecConfig{ + Command: "okteto", + Args: []string{"kubetoken", oktetoURL, namespace}, + APIVersion: "client.authentication.k8s.io/v1", + InstallHint: "Okteto needs to be installed and in your PATH to use this context. Please visit https://www.okteto.com/docs/getting-started/ for more information.", + ProvideClusterInfo: true, + InteractiveMode: "IfAvailable", + } + } else { + // fallback for okteto API before client authentication support + // TODO: remove once we stop supporting token based authentication https://github.com/okteto/okteto/pull/3409 + user.Token = cred.Token + user.Exec = nil + } cfg.AuthInfos[userName] = user // create context @@ -379,6 +400,7 @@ func AddOktetoCredentialsToCfg(cfg *clientcmdapi.Config, cred *types.Credential, cfg.Contexts[clusterName] = context cfg.CurrentContext = clusterName + return nil } func GetK8sClient() (*kubernetes.Clientset, *rest.Config, error) { diff --git a/pkg/okteto/context_test.go b/pkg/okteto/context_test.go index ccb77f521d67..99e02602645b 100644 --- a/pkg/okteto/context_test.go +++ b/pkg/okteto/context_test.go @@ -15,9 +15,16 @@ package okteto import ( "context" + "fmt" "testing" + "k8s.io/apimachinery/pkg/runtime" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "github.com/okteto/okteto/internal/test" + "github.com/okteto/okteto/pkg/constants" + "github.com/okteto/okteto/pkg/types" + "github.com/stretchr/testify/require" ) func Test_UrlToKubernetesContext(t *testing.T) { @@ -122,3 +129,126 @@ func Test_RemoveSchema(t *testing.T) { }) } } + +func Test_AddOktetoCredentialsToCfg(t *testing.T) { + t.Parallel() + + cfg := clientcmdapi.NewConfig() + namespace := "namespace" + username := "cindy" + cred := &types.Credential{ + Server: "https://ip.ip.ip.ip", + Certificate: "cred-cert", + Token: "cred-token", + } + + oktetoURL := "https://cloud.okteto.com" + clusterAndContextName := "cloud_okteto_com" + + tt := []struct { + name string + hasKubeToken bool + expectedCfg *clientcmdapi.Config + err error + expectedError error + }{ + { + name: "has-kube-token", + hasKubeToken: true, + expectedCfg: &clientcmdapi.Config{ + Extensions: map[string]runtime.Object{}, + Clusters: map[string]*clientcmdapi.Cluster{ + clusterAndContextName: { + Server: cred.Server, + CertificateAuthorityData: []byte(cred.Certificate), + Extensions: map[string]runtime.Object{}, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + clusterAndContextName: { + Cluster: clusterAndContextName, + AuthInfo: username, + Namespace: namespace, + Extensions: map[string]runtime.Object{ + constants.OktetoExtension: nil, + }, + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + username: { + Exec: &clientcmdapi.ExecConfig{ + Command: "okteto", + Args: []string{"kubetoken", oktetoURL, namespace}, + APIVersion: "client.authentication.k8s.io/v1", + InstallHint: "Okteto needs to be installed and in your PATH to use this context. Please visit https://www.okteto.com/docs/getting-started/ for more information.", + ProvideClusterInfo: true, + InteractiveMode: "IfAvailable", + }, + Extensions: map[string]runtime.Object{}, + ImpersonateUserExtra: map[string][]string{}, + }, + }, + CurrentContext: clusterAndContextName, + Preferences: clientcmdapi.Preferences{ + Extensions: map[string]runtime.Object{}, + }, + }, + }, + { + name: "no-kube-token", + hasKubeToken: false, + expectedCfg: &clientcmdapi.Config{ + Extensions: map[string]runtime.Object{}, + Clusters: map[string]*clientcmdapi.Cluster{ + clusterAndContextName: { + Server: cred.Server, + CertificateAuthorityData: []byte(cred.Certificate), + Extensions: map[string]runtime.Object{}, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + clusterAndContextName: { + Cluster: clusterAndContextName, + AuthInfo: username, + Namespace: namespace, + Extensions: map[string]runtime.Object{ + constants.OktetoExtension: nil, + }, + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + username: { + Token: cred.Token, + Extensions: map[string]runtime.Object{}, + ImpersonateUserExtra: map[string][]string{}, + }, + }, + CurrentContext: clusterAndContextName, + Preferences: clientcmdapi.Preferences{ + Extensions: map[string]runtime.Object{}, + }, + }, + }, + { + name: "error", + err: fmt.Errorf("error"), + expectedError: fmt.Errorf("error"), + expectedCfg: clientcmdapi.NewConfig(), + }, + } + + for _, testCase := range tt { + cfg := cfg.DeepCopy() + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + hasKubeTokenCapabily := func(oktetoURL string) (bool, error) { + return testCase.hasKubeToken, testCase.err + } + err := AddOktetoCredentialsToCfg(cfg, cred, namespace, username, oktetoURL, hasKubeTokenCapabily) + require.Equal(t, testCase.expectedCfg, cfg) + require.Equal(t, testCase.expectedError, err) + }) + } + +} diff --git a/pkg/types/credential.go b/pkg/types/credential.go index 5456fa085039..738f1e156bfd 100644 --- a/pkg/types/credential.go +++ b/pkg/types/credential.go @@ -17,6 +17,6 @@ package types type Credential struct { Server string `json:"server" yaml:"server"` Certificate string `json:"certificate" yaml:"certificate"` - Token string `json:"token" yaml:"token"` + Token string `json:"token" yaml:"token"` // TODO: Deprecate once we move to kubetoken auth Namespace string `json:"namespace" yaml:"namespace"` }