Skip to content

Commit

Permalink
OIDC: allow retrieving kubeconfig from BTP (#2124)
Browse files Browse the repository at this point in the history
  • Loading branch information
halamix2 authored Jun 3, 2024
1 parent 362cd9c commit 4e2198a
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 54 deletions.
80 changes: 80 additions & 0 deletions internal/btp/cis/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cis

import (
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/kyma-project/cli.v3/internal/clierror"
)

const environmentsEndpoint = "provisioning/v1/environments"

type Labels struct {
APIServerURL string `json:"APIServerURL"`
KubeconfigURL string `json:"KubeconfigURL"`
Name string `json:"Name"`
}

type environmentInstances struct {
EnvironmentInstances []ProvisionResponse `json:"environmentInstances"`
}

func (c *LocalClient) GetKymaKubeconfig() (string, clierror.Error) {
provisionURL := fmt.Sprintf("%s/%s", c.credentials.Endpoints.ProvisioningServiceURL, environmentsEndpoint)

response, err := c.cis.get(provisionURL, requestOptions{})
if err != nil {
// TODO: finish - error codes?
return "", clierror.New(err.Error())
}

defer response.Body.Close()

return decodeResponse(response)

}

func decodeResponse(response *http.Response) (string, clierror.Error) {
envInstances := environmentInstances{}
err := json.NewDecoder(response.Body).Decode(&envInstances)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to decode response"))
}

// we assume there can be only one Kyma environment in the BTP subaccount
for _, env := range envInstances.EnvironmentInstances {
if env.EnvironmentType == "kyma" {
// parse labels to get kubeconfig URL
labels := Labels{}
err := json.Unmarshal([]byte(env.Labels), &labels)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to unmarshal labels"))
}

kubeconfig, err := downloadKubeconfig(labels.KubeconfigURL)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to get kubeconfig"))
}
return kubeconfig, nil
}
}

return "", clierror.New("no Kyma environment found")
}

func downloadKubeconfig(url string) (string, error) {
response, err := http.Get(url)
if err != nil {
return "", err
}
defer response.Body.Close()

kubeconfig, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}

return string(kubeconfig), nil
}
138 changes: 84 additions & 54 deletions internal/cmd/oidc/oidc.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
package oidc

import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"

"github.com/kyma-project/cli.v3/internal/btp/auth"
"github.com/kyma-project/cli.v3/internal/btp/cis"
"github.com/kyma-project/cli.v3/internal/clierror"
"github.com/kyma-project/cli.v3/internal/cmdcommon"
"github.com/kyma-project/cli.v3/internal/kube"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)

type oidcConfig struct {
*cmdcommon.KymaConfig
cmdcommon.KubeClientConfig

cisCredentialsPath string
output string
caCertificate string
clusterServer string
audience string
token string
idTokenRequestURL string
Expand Down Expand Up @@ -53,17 +55,15 @@ func NewOIDCCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {

cfg.KubeClientConfig.AddFlag(cmd)

cmd.Flags().StringVar(&cfg.cisCredentialsPath, "credentials-path", "", "Path to the CIS credentials file.")
cmd.Flags().StringVar(&cfg.output, "output", "", "Path to the output kubeconfig file")
cmd.Flags().StringVar(&cfg.caCertificate, "ca-certificate", "", "Path to the CA certificate file")
cmd.Flags().StringVar(&cfg.clusterServer, "cluster-server", "", "URL of the cluster server")

cmd.Flags().StringVar(&cfg.token, "token", "", "Token used in the kubeconfig")
cmd.Flags().StringVar(&cfg.audience, "audience", "", "Audience of the token")
cmd.Flags().StringVar(&cfg.idTokenRequestURL, "id-token-request-url", "", "URL to request the ID token, defaults to ACTIONS_ID_TOKEN_REQUEST_URL env variable")

cmd.MarkFlagsOneRequired("kubeconfig", "ca-certificate")
cmd.MarkFlagsRequiredTogether("ca-certificate", "cluster-server")
cmd.MarkFlagsMutuallyExclusive("kubeconfig", "ca-certificate")
cmd.MarkFlagsOneRequired("kubeconfig", "credentials-path")
cmd.MarkFlagsMutuallyExclusive("kubeconfig", "credentials-path")

cmd.MarkFlagsMutuallyExclusive("token", "id-token-request-url")
cmd.MarkFlagsMutuallyExclusive("token", "audience")
Expand All @@ -77,7 +77,7 @@ func (cfg *oidcConfig) complete() clierror.Error {
}
cfg.idTokenRequestToken = os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")

if cfg.KubeClientConfig.Kubeconfig != "" {
if cfg.cisCredentialsPath == "" {
return cfg.KubeClientConfig.Complete()
}
return nil
Expand All @@ -91,58 +91,101 @@ func (cfg *oidcConfig) validate() clierror.Error {

if cfg.idTokenRequestURL == "" {
return clierror.New(
"ID token request URL is required",
"ID token request URL is required if --token is not provided",
"make sure you're running the command in Github Actions environment",
"provide id-token-request-url flag or ACTIONS_ID_TOKEN_REQUEST_URL env variable",
)
}

if cfg.idTokenRequestToken == "" {
return clierror.New(
"ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable is required",
"ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable is required if --token is not provided",
"make sure you're running the command in Github Actions environment",
)
}
return nil
}

func runOIDC(cfg *oidcConfig) clierror.Error {
var err error
var clierr clierror.Error
token := cfg.token
if cfg.token != "" {
if cfg.token == "" {
// get Github token
token, err = getGithubToken(cfg.idTokenRequestURL, cfg.idTokenRequestToken, cfg.audience)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to get token"))
token, clierr = getGithubToken(cfg.idTokenRequestURL, cfg.idTokenRequestToken, cfg.audience)
if clierr != nil {
return clierror.WrapE(clierr, clierror.New("failed to get token"))
}
}
caCertificate := cfg.caCertificate
clusterServer := cfg.clusterServer
if cfg.KubeClientConfig.Kubeconfig != "" {
currentServer := cfg.KubeClient.ApiConfig().Clusters[cfg.KubeClient.ApiConfig().CurrentContext]
caCertificate = string(currentServer.CertificateAuthorityData)
clusterServer = currentServer.Server
}

enrichedKubeconfig, err := createKubeconfig(caCertificate, clusterServer, token)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to create kubeconfig"))
var kubeconfig *api.Config

if cfg.cisCredentialsPath != "" {
kubeconfig, clierr = getKubeconfigFromCIS(cfg)
if clierr != nil {
return clierror.WrapE(clierr, clierror.New("failed to get kubeconfig from CIS"))
}
} else {
kubeconfig = cfg.KubeClient.ApiConfig()
}

err = kube.SaveConfig(enrichedKubeconfig, cfg.output)
enrichedKubeconfig := createKubeconfig(kubeconfig, token)

err := kube.SaveConfig(enrichedKubeconfig, cfg.output)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to save kubeconfig"))
}

return nil
}

func getGithubToken(url, requestToken, audience string) (string, error) {
func getKubeconfigFromCIS(cfg *oidcConfig) (*api.Config, clierror.Error) {
// TODO: maybe refactor with provision command to not duplicate localCISClient provisioning
credentials, err := auth.LoadCISCredentials(cfg.cisCredentialsPath)
if err != nil {
return nil, err
}
token, err := auth.GetOAuthToken(
credentials.GrantType,
credentials.UAA.URL,
credentials.UAA.ClientID,
credentials.UAA.ClientSecret,
)
if err != nil {
var hints []string
if strings.Contains(err.String(), "Internal Server Error") {
hints = append(hints, "check if CIS grant type is set to client credentials")
}

return nil, clierror.WrapE(err, clierror.New("failed to get access token", hints...))
}

localCISClient := cis.NewLocalClient(credentials, token)
kubeconfigString, err := localCISClient.GetKymaKubeconfig()
if err != nil {
return nil, clierror.WrapE(err, clierror.New("failed to get kubeconfig"))
}

kubeconfig, err := parseKubeconfig(kubeconfigString)
if err != nil {
return nil, clierror.WrapE(err, clierror.New("failed to parse kubeconfig"))
}
return kubeconfig, nil
}

func parseKubeconfig(kubeconfigString string) (*api.Config, clierror.Error) {
kubeconfig, err := clientcmd.Load([]byte(kubeconfigString))
if err != nil {
return nil, clierror.Wrap(err, clierror.New("failed to parse kubeconfig string"))
}
return kubeconfig, nil
}

func getGithubToken(url, requestToken, audience string) (string, clierror.Error) {
// create http client

request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", err
return "", clierror.Wrap(err, clierror.New("failed to create request"))
}
if audience != "" {
q := request.URL.Query()
Expand All @@ -155,51 +198,38 @@ func getGithubToken(url, requestToken, audience string) (string, error) {

response, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
return "", clierror.Wrap(err, clierror.New("failed to get token from Github"))
}
defer response.Body.Close()

if response.StatusCode != 200 {
return "", fmt.Errorf("failed to get token from server: %s", response.Status)
return "", clierror.New(fmt.Sprintf("Invalid server response: %d", response.StatusCode))
}

tokenData := TokenData{}
err = json.NewDecoder(response.Body).Decode(&tokenData)
if err != nil {
return "", err
return "", clierror.Wrap(err, clierror.New("failed to decode token response"))
}
return tokenData.Value, nil
}

func createKubeconfig(caCertificate, clusterServer, token string) (*api.Config, error) {
certificate, err := base64.StdEncoding.DecodeString(caCertificate)
if err != nil {
return nil, err
}

func createKubeconfig(kubeconfig *api.Config, token string) *api.Config {
currentUser := kubeconfig.Contexts[kubeconfig.CurrentContext].AuthInfo
config := &api.Config{
Kind: "Config",
APIVersion: "v1",
Clusters: map[string]*api.Cluster{
"cluster": {
Server: clusterServer,
CertificateAuthorityData: certificate,
},
},
Clusters: kubeconfig.Clusters,
AuthInfos: map[string]*api.AuthInfo{
"user": {
currentUser: {
Token: token,
},
},
Contexts: map[string]*api.Context{
"default": {
Cluster: "cluster",
AuthInfo: "user",
},
},
CurrentContext: "default",
Extensions: nil,
Contexts: kubeconfig.Contexts,
CurrentContext: kubeconfig.CurrentContext,
Extensions: kubeconfig.Extensions,
Preferences: kubeconfig.Preferences,
}

return config, nil
return config
}

0 comments on commit 4e2198a

Please sign in to comment.