diff --git a/CHANGELOG.md b/CHANGELOG.md index 254640bd0..a3721aca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project's packages adheres to [Semantic Versioning](http://semver.org/s ## [Unreleased] +### Added + +- Adding `opsctl login` support for EKS clusters. + ## [2.40.0] - 2023-08-09 ### Added diff --git a/cmd/login/aws.go b/cmd/login/aws.go new file mode 100644 index 000000000..1fd11141a --- /dev/null +++ b/cmd/login/aws.go @@ -0,0 +1,228 @@ +package login + +import ( + "context" + "fmt" + + "github.com/giantswarm/k8sclient/v7/pkg/k8sclient" + "github.com/giantswarm/microerror" + "github.com/spf13/afero" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + eks "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/giantswarm/kubectl-gs/v2/pkg/kubeconfig" +) + +type eksClusterConfig struct { + clusterName string + certCA []byte + controlPlaneEndpoint string + filePath string + loginOptions LoginOptions + region string + + awsProfileName string +} + +// storeWCClientCertCredentials saves the created client certificate credentials into the kubectl config. +func storeWCAWSIAMKubeconfig(k8sConfigAccess clientcmd.ConfigAccess, c eksClusterConfig, mcContextName string) (string, bool, error) { + config, err := k8sConfigAccess.GetStartingConfig() + if err != nil { + return "", false, microerror.Mask(err) + } + + if mcContextName == "" { + mcContextName = config.CurrentContext + } + contextName := kubeconfig.GenerateWCAWSIAMKubeContextName(mcContextName, c.clusterName) + userName := fmt.Sprintf("%s-user", contextName) + clusterName := contextName + + contextExists := false + + { + // Create authenticated user. + user, exists := config.AuthInfos[userName] + if !exists { + user = clientcmdapi.NewAuthInfo() + } + + user.Exec = awsIAMExec(c.clusterName, c.region) + + if c.awsProfileName != "" { + user.Exec.Env = []clientcmdapi.ExecEnvVar{ + { + Name: "AWS_DEFAULT_PROFILE", + Value: c.awsProfileName, + }, + } + } + // Add user information to config. + config.AuthInfos[userName] = user + } + + { + // Create authenticated cluster. + cluster, exists := config.Clusters[clusterName] + if !exists { + cluster = clientcmdapi.NewCluster() + } + + cluster.Server = c.controlPlaneEndpoint + cluster.CertificateAuthority = "" + cluster.CertificateAuthorityData = c.certCA + + // Add cluster configuration to config. + config.Clusters[clusterName] = cluster + } + + { + // Create authenticated context. + var context *clientcmdapi.Context + context, contextExists = config.Contexts[contextName] + if !contextExists { + context = clientcmdapi.NewContext() + } + + context.Cluster = clusterName + context.AuthInfo = userName + + // Add context configuration to config. + config.Contexts[contextName] = context + + // Select newly created context as current or revert to origin context if that is desired + if c.loginOptions.switchToWCContext { + config.CurrentContext = contextName + } else if c.loginOptions.originContext != "" { + config.CurrentContext = c.loginOptions.originContext + } + } + + err = clientcmd.ModifyConfig(k8sConfigAccess, *config, false) + if err != nil { + return "", contextExists, microerror.Mask(err) + } + + return contextName, contextExists, nil +} + +// printWCAWSIamCredentials saves the created client certificate credentials into a separate kubectl config file. +func printWCAWSIamCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afero.Fs, c eksClusterConfig, mcContextName string) (string, bool, error) { + config, err := k8sConfigAccess.GetStartingConfig() + if err != nil { + return "", false, microerror.Mask(err) + } + + if mcContextName == "" { + mcContextName = config.CurrentContext + } + contextName := kubeconfig.GenerateWCAWSIAMKubeContextName(mcContextName, c.clusterName) + + kubeconfig := clientcmdapi.Config{ + APIVersion: "v1", + Kind: "Config", + Clusters: map[string]*clientcmdapi.Cluster{ + contextName: { + Server: c.controlPlaneEndpoint, + CertificateAuthorityData: c.certCA, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + contextName: { + Cluster: contextName, + AuthInfo: fmt.Sprintf("%s-user", contextName), + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + fmt.Sprintf("%s-user", contextName): { + Exec: awsIAMExec(c.clusterName, c.region), + }, + }, + CurrentContext: contextName, + } + if c.awsProfileName != "" { + kubeconfig.AuthInfos[fmt.Sprintf("%s-user", contextName)].Exec.Env = []clientcmdapi.ExecEnvVar{ + { + Name: "AWS_DEFAULT_PROFILE", + Value: c.awsProfileName, + }, + } + } + + err = mergeKubeconfigs(fs, c.filePath, kubeconfig, contextName) + if err != nil { + return "", false, microerror.Mask(err) + } + + // Change back to the origin context if needed + if c.loginOptions.originContext != "" && config.CurrentContext != "" && c.loginOptions.originContext != config.CurrentContext { + // Because we are still in the MC context we need to switch back to the origin context after creating the WC kubeconfig file + config.CurrentContext = c.loginOptions.originContext + err = clientcmd.ModifyConfig(k8sConfigAccess, *config, false) + if err != nil { + return "", false, microerror.Mask(err) + } + } + + return contextName, false, nil +} + +type kubeconfigFile struct { + Clusters []kubeCluster `json:"clusters"` +} + +type kubeCluster struct { + Cluster kubeClusterSpec `json:"cluster"` +} + +type kubeClusterSpec struct { + CertificateAuthorityData []byte `json:"certificate-authority-data"` +} + +func fetchEKSCAData(ctx context.Context, c k8sclient.Interface, clusterName string, clusterNamespace string) ([]byte, error) { + var secret v1.Secret + err := c.CtrlClient().Get(ctx, client.ObjectKey{Name: eksKubeconfigSecretName(clusterName), Namespace: clusterNamespace}, &secret) + if err != nil { + return nil, microerror.Mask(err) + } + + secretData := secret.Data["value"] + + kConfig := &kubeconfigFile{} + + err = yaml.Unmarshal(secretData, kConfig) + if err != nil { + return nil, microerror.Mask(err) + } + return kConfig.Clusters[0].Cluster.CertificateAuthorityData, err +} + +func fetchEKSRegion(ctx context.Context, c k8sclient.Interface, clusterName string, clusterNamespace string) (string, error) { + var eksCluster eks.AWSManagedControlPlane + err := c.CtrlClient().Get(ctx, client.ObjectKey{Name: clusterName, Namespace: clusterNamespace}, &eksCluster) + if err != nil { + return "", microerror.Mask(err) + } + + return eksCluster.Spec.Region, nil +} + +func eksKubeconfigSecretName(clusterName string) string { + return fmt.Sprintf("%s-user-kubeconfig", clusterName) +} + +func awsIAMExec(clusterName string, region string) *clientcmdapi.ExecConfig { + return &clientcmdapi.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1beta1", + Command: "aws", + Args: awsKubeconfigExecArgs(clusterName, region), + } +} + +func awsKubeconfigExecArgs(clusterName string, region string) []string { + return []string{"--region", region, "eks", "get-token", "--cluster-name", clusterName, "--output", "json"} +} diff --git a/cmd/login/clientcert.go b/cmd/login/clientcert.go index 57ad7a8f4..689f1de24 100644 --- a/cmd/login/clientcert.go +++ b/cmd/login/clientcert.go @@ -63,7 +63,7 @@ type serviceSet struct { releaseService release.Interface } -type credentialConfig struct { +type clientCertCredentialConfig struct { clusterID string certCRT []byte certKey []byte @@ -269,7 +269,7 @@ func getPrivKey(keyPEM []byte) (*rsa.PrivateKey, error) { } // storeWCClientCertCredentials saves the created client certificate credentials into the kubectl config. -func storeWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afero.Fs, c credentialConfig, mcContextName string) (string, bool, error) { +func storeWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, c clientCertCredentialConfig, mcContextName string) (string, bool, error) { config, err := k8sConfigAccess.GetStartingConfig() if err != nil { return "", false, microerror.Mask(err) @@ -332,7 +332,7 @@ func storeWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afe config.Contexts[contextName] = context // Select newly created context as current or revert to origin context if that is desired - if c.loginOptions.switchToClientCertContext { + if c.loginOptions.switchToWCContext { config.CurrentContext = contextName } else if c.loginOptions.originContext != "" { config.CurrentContext = c.loginOptions.originContext @@ -348,7 +348,7 @@ func storeWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afe } // printWCClientCertCredentials saves the created client certificate credentials into a separate kubectl config file. -func printWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afero.Fs, c credentialConfig, mcContextName string) (string, bool, error) { +func printWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afero.Fs, c clientCertCredentialConfig, mcContextName string) (string, bool, error) { config, err := k8sConfigAccess.GetStartingConfig() if err != nil { return "", false, microerror.Mask(err) @@ -382,16 +382,36 @@ func printWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afe }, CurrentContext: contextName, } - // If the destination file exists, we merge the contexts contained in it with the newly created one - exists, err := afero.Exists(fs, c.filePath) + + err = mergeKubeconfigs(fs, c.filePath, kubeconfig, contextName) if err != nil { return "", false, microerror.Mask(err) } - if exists { - existingKubeConfig, err := clientcmd.LoadFromFile(c.filePath) + + // Change back to the origin context if needed + if c.loginOptions.originContext != "" && config.CurrentContext != "" && c.loginOptions.originContext != config.CurrentContext { + // Because we are still in the MC context we need to switch back to the origin context after creating the WC kubeconfig file + config.CurrentContext = c.loginOptions.originContext + err = clientcmd.ModifyConfig(k8sConfigAccess, *config, false) if err != nil { return "", false, microerror.Mask(err) } + } + + return contextName, false, nil +} + +func mergeKubeconfigs(fs afero.Fs, filePath string, kubeconfig clientcmdapi.Config, contextName string) error { + // If the destination file exists, we merge the contexts contained in it with the newly created one + exists, err := afero.Exists(fs, filePath) + if err != nil { + return microerror.Mask(err) + } + if exists { + existingKubeConfig, err := clientcmd.LoadFromFile(filePath) + if err != nil { + return microerror.Mask(err) + } // First remove entries included in the new config from the existing one for clusterName := range kubeconfig.Clusters { @@ -407,25 +427,16 @@ func printWCClientCertCredentials(k8sConfigAccess clientcmd.ConfigAccess, fs afe // Then merge the 2 configs (entries from the new config will be added to the existing one) err = mergo.Merge(&kubeconfig, existingKubeConfig, mergo.WithOverride) if err != nil { - return "", false, microerror.Mask(err) + return microerror.Mask(err) } kubeconfig.CurrentContext = contextName } - err = clientcmd.WriteToFile(kubeconfig, c.filePath) + err = clientcmd.WriteToFile(kubeconfig, filePath) if err != nil { - return "", false, microerror.Mask(err) - } - // Change back to the origin context if needed - if c.loginOptions.originContext != "" && config.CurrentContext != "" && c.loginOptions.originContext != config.CurrentContext { - // Because we are still in the MC context we need to switch back to the origin context after creating the WC kubeconfig file - config.CurrentContext = c.loginOptions.originContext - err = clientcmd.ModifyConfig(k8sConfigAccess, *config, false) - if err != nil { - return "", false, microerror.Mask(err) - } + return microerror.Mask(err) } - return contextName, false, nil + return nil } func cleanUpClientCertResources(ctx context.Context, clientCertService clientcert.Interface, clientCertResource *clientcert.ClientCert) error { diff --git a/cmd/login/clientcert_test.go b/cmd/login/clientcert_test.go index 38804dbd2..65a00ec7a 100644 --- a/cmd/login/clientcert_test.go +++ b/cmd/login/clientcert_test.go @@ -20,13 +20,13 @@ func Test_ClientCert_SelfContainedFiles(t *testing.T) { name string fileName string sourceConfig *clientcmdapi.Config - credentialConfig credentialConfig + credentialConfig clientCertCredentialConfig expectedConfig clientcmdapi.Config }{ { name: "case 0: Create a new self-contained file", fileName: "cluster.yaml", - credentialConfig: credentialConfig{ + credentialConfig: clientCertCredentialConfig{ clusterID: "cluster", certCRT: []byte("CertCRT"), certKey: []byte("CertKey"), @@ -79,7 +79,7 @@ func Test_ClientCert_SelfContainedFiles(t *testing.T) { }, CurrentContext: "initial-context", }, - credentialConfig: credentialConfig{ + credentialConfig: clientCertCredentialConfig{ clusterID: "cluster", certCRT: []byte("CertCRT"), certKey: []byte("CertKey"), @@ -156,7 +156,7 @@ func Test_ClientCert_SelfContainedFiles(t *testing.T) { }, CurrentContext: "gs-codename-cluster-clientcert", }, - credentialConfig: credentialConfig{ + credentialConfig: clientCertCredentialConfig{ clusterID: "cluster", certCRT: []byte("NewCertCRT"), certKey: []byte("NewCertKey"), @@ -221,7 +221,7 @@ func Test_ClientCert_SelfContainedFiles(t *testing.T) { }, CurrentContext: "gs-codename-cluster-clientcert", }, - credentialConfig: credentialConfig{ + credentialConfig: clientCertCredentialConfig{ clusterID: "cluster", certCRT: []byte("NewCertCRT"), certKey: []byte("NewCertKey"), diff --git a/cmd/login/flag.go b/cmd/login/flag.go index b11c47d8d..dfe633645 100644 --- a/cmd/login/flag.go +++ b/cmd/login/flag.go @@ -27,6 +27,8 @@ const ( flagProxy = "proxy" flagProxyPort = "proxy-port" + flagAwsProfile = "aws-profile" + flagLoginTimeout = "login-timeout" ) @@ -49,6 +51,8 @@ type flag struct { Proxy bool ProxyPort int + AWSProfile string + LoginTimeout time.Duration } @@ -71,6 +75,8 @@ func (f *flag) Init(cmd *cobra.Command) { cmd.Flags().BoolVar(&f.Proxy, flagProxy, false, "Enable socks proxy configuration for the cluster. Only Supported for Workload Cluster using clientcert auth mode") cmd.Flags().IntVar(&f.ProxyPort, flagProxyPort, 9000, "Port for the socks proxy configuration for the cluster") + cmd.Flags().StringVar(&f.AWSProfile, flagAwsProfile, "", "AWS profile name that the created kubeconfig will always use, Only applicable for EKS clusters.") + cmd.Flags().DurationVar(&f.LoginTimeout, flagLoginTimeout, 60*time.Second, "Duration for which kubectl gs will wait for the OIDC login to complete. Once the timeout is reached, OIDC login will fail.") _ = cmd.Flags().MarkHidden(flagWCInsecureNamespace) diff --git a/cmd/login/login.go b/cmd/login/login.go index 56114954d..80e2cde0f 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -61,7 +61,7 @@ func (r *runner) loginWithKubeContextName(ctx context.Context, contextName strin if contextAlreadySelected { fmt.Fprintf(r.stdout, "Context '%s' is already selected.\n", contextName) - } else if !r.loginOptions.isWCClientCert && r.loginOptions.switchToContext { + } else if !r.loginOptions.isWC && r.loginOptions.switchToContext { fmt.Fprintf(r.stdout, "Switched to context '%s'.\n", contextName) } diff --git a/cmd/login/runner.go b/cmd/login/runner.go index ec0529b04..b7401aac8 100644 --- a/cmd/login/runner.go +++ b/cmd/login/runner.go @@ -29,13 +29,13 @@ type runner struct { } type LoginOptions struct { - isWCClientCert bool - selfContained bool - selfContainedClientCert bool - switchToContext bool - switchToClientCertContext bool - originContext string - contextOverride string + selfContained bool + selfContainedWC bool // used only if both MC and WC are specified on command line + isWC bool // used only if both MC and WC are specified on command line + switchToContext bool + switchToWCContext bool // used only if both MC and WC are specified on command line + originContext string + contextOverride string } func (r *runner) Run(cmd *cobra.Command, args []string) error { @@ -100,9 +100,12 @@ func (r *runner) run(ctx context.Context, cmd *cobra.Command, args []string) err return microerror.Maskf(invalidConfigError, "Invalid number of arguments.") } - // Clientcert creation if desired - if r.loginOptions.isWCClientCert { - return r.handleWCClientCert(ctx) + // used only if both MC and WC are specified on command line + if r.loginOptions.isWC { + err := r.handleWCKubeconfig(ctx) + if err != nil { + return microerror.Mask(err) + } } return nil @@ -137,13 +140,13 @@ func (r *runner) setLoginOptions(ctx context.Context, args *[]string) { shouldSwitchToWCContextInConfig := hasWCNameFlag && !(hasSelfContainedFlag || r.flag.KeepContext) r.loginOptions = LoginOptions{ - originContext: originContext, - contextOverride: contextOverride, - isWCClientCert: hasWCNameFlag, - selfContained: hasSelfContainedFlag && !hasWCNameFlag, - selfContainedClientCert: hasSelfContainedFlag && hasWCNameFlag, - switchToContext: shouldSwitchContextInConfig, - switchToClientCertContext: shouldSwitchToWCContextInConfig, + originContext: originContext, + contextOverride: contextOverride, + isWC: hasWCNameFlag, + selfContained: hasSelfContainedFlag && !hasWCNameFlag, + selfContainedWC: hasSelfContainedFlag && hasWCNameFlag, + switchToContext: shouldSwitchContextInConfig, + switchToWCContext: shouldSwitchToWCContextInConfig, } } diff --git a/cmd/login/wc.go b/cmd/login/wc.go index cd0b94567..482718fad 100644 --- a/cmd/login/wc.go +++ b/cmd/login/wc.go @@ -10,6 +10,7 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/clientcmd" + capi "sigs.k8s.io/cluster-api/api/v1beta1" "github.com/fatih/color" "github.com/giantswarm/k8sclient/v7/pkg/k8sclient" @@ -119,7 +120,8 @@ func (r *runner) getCertOperatorVersion(c *cluster.Cluster, provider string, ser return key.CertOperatorVersionKubeconfig, nil } -func (r *runner) handleWCClientCert(ctx context.Context) error { +// used only if both MC and WC are specified on command line +func (r *runner) handleWCKubeconfig(ctx context.Context) error { provider, err := r.commonConfig.GetProviderFromConfig(ctx, "") if err != nil { return microerror.Mask(err) @@ -132,8 +134,7 @@ func (r *runner) handleWCClientCert(ctx context.Context) error { return microerror.Mask(err) } } - // At the moment, the only available login option for WC is client cert - contextName, contextExists, err := r.createClusterClientCert(ctx, client, provider) + contextName, contextExists, err := r.createClusterKubeconfig(ctx, client, provider) if err != nil { if IsClusterAPINotReady(err) { fmt.Fprintf(r.stdout, "\nCould not create a context for workload cluster %s, as the cluster's API server endpoint is not known yet.\n", r.flag.WCName) @@ -145,10 +146,10 @@ func (r *runner) handleWCClientCert(ctx context.Context) error { return microerror.Mask(err) } - if r.loginOptions.selfContainedClientCert { + if r.loginOptions.selfContainedWC { fmt.Fprintf(r.stdout, "A new kubectl context has been created named '%s' and stored in '%s'. You can select this context like this:\n\n", contextName, r.flag.SelfContained) fmt.Fprintf(r.stdout, " kubectl cluster-info --kubeconfig %s \n", r.flag.SelfContained) - } else if !r.loginOptions.switchToClientCertContext { + } else if !r.loginOptions.switchToWCContext { fmt.Fprintf(r.stdout, "A new kubectl context has been created named '%s'. To switch back to this context later, use this command:\n\n", contextName) fmt.Fprintf(r.stdout, " kubectl config use-context %s\n", contextName) } else if contextExists { @@ -163,7 +164,8 @@ func (r *runner) handleWCClientCert(ctx context.Context) error { return nil } -func (r *runner) createClusterClientCert(ctx context.Context, client k8sclient.Interface, provider string) (contextName string, contextExists bool, err error) { +// used only if both MC and WC are specified on command line +func (r *runner) createClusterKubeconfig(ctx context.Context, client k8sclient.Interface, provider string) (contextName string, contextExists bool, err error) { err = validateProvider(provider) if err != nil { return "", false, microerror.Mask(err) @@ -179,6 +181,25 @@ func (r *runner) createClusterClientCert(ctx context.Context, client k8sclient.I return "", false, microerror.Mask(err) } + // for EKS the kubeconfig cannot be client-cert as it uses aws authentication + // for the rest of WC cluster client-cert kubeconfig will be generated + if isEKS(c.Cluster) { + contextName, contextExists, err = r.createEKSKubeconfig(ctx, client, c) + if err != nil { + return "", false, microerror.Mask(err) + } + } else { + contextName, contextExists, err = r.createCertKubeconfig(ctx, c, services, provider) + if err != nil { + return "", false, microerror.Mask(err) + } + } + + return contextName, contextExists, nil +} + +// used only if both MC and WC are specified on command line +func (r *runner) createCertKubeconfig(ctx context.Context, c *cluster.Cluster, services serviceSet, provider string) (string, bool, error) { certOperatorVersion, err := r.getCertOperatorVersion(c, provider, services, ctx) if err != nil { return "", false, microerror.Mask(err) @@ -221,7 +242,7 @@ func (r *runner) createClusterClientCert(ctx context.Context, client k8sclient.I clusterServer = fmt.Sprintf("https://api.%s.%s:%d", c.Cluster.Name, clusterBasePath, c.Cluster.Spec.ControlPlaneEndpoint.Port) } - credentialConfig := credentialConfig{ + credentialConfig := clientCertCredentialConfig{ clusterID: r.flag.WCName, certCRT: secret.Data[credentialKeyCertCRT], certKey: secret.Data[credentialKeyCertKey], @@ -233,7 +254,7 @@ func (r *runner) createClusterClientCert(ctx context.Context, client k8sclient.I proxyPort: r.flag.ProxyPort, } - contextName, contextExists, err = r.storeWCClientCertCredentials(credentialConfig) + contextName, contextExists, err := r.storeWCClientCertCredentials(credentialConfig) if err != nil { return "", false, microerror.Mask(err) } @@ -248,6 +269,39 @@ func (r *runner) createClusterClientCert(ctx context.Context, client k8sclient.I } fmt.Fprint(r.stdout, color.GreenString("\nCreated client certificate for workload cluster '%s'.\n", r.flag.WCName)) + + return contextName, contextExists, nil +} + +func (r *runner) createEKSKubeconfig(ctx context.Context, k8sClient k8sclient.Interface, c *cluster.Cluster) (string, bool, error) { + caData, err := fetchEKSCAData(ctx, k8sClient, c.Cluster.Name, c.Cluster.Namespace) + if err != nil { + return "", false, microerror.Mask(err) + } + + region, err := fetchEKSRegion(ctx, k8sClient, c.Cluster.Name, c.Cluster.Namespace) + if err != nil { + return "", false, microerror.Mask(err) + } + + eksClusterConfig := eksClusterConfig{ + awsProfileName: r.flag.AWSProfile, + clusterName: c.Cluster.Name, + certCA: caData, + controlPlaneEndpoint: c.Cluster.Spec.ControlPlaneEndpoint.Host, + filePath: r.flag.SelfContained, + region: region, + loginOptions: r.loginOptions, + } + + contextName, contextExists, err := r.storeAWSIAMCredentials(eksClusterConfig) + if err != nil { + return "", false, microerror.Mask(err) + } + + fmt.Fprint(r.stdout, color.GreenString("\nCreated aws IAM based kubeconfig for EKS workload cluster '%s'.\n", r.flag.WCName)) + fmt.Fprint(r.stdout, color.YellowString("\nRemember to have valid and active AWS credentials that have 'eks:GetToken' permissions on the EKS cluster resource in order to use the kubeconfig.\n\n")) + return contextName, contextExists, nil } @@ -287,13 +341,21 @@ func (r *runner) getCredentials(ctx context.Context, clientCertService clientcer return clientCert, clientCertsecret, nil } -func (r *runner) storeWCClientCertCredentials(c credentialConfig) (string, bool, error) { +func (r *runner) storeWCClientCertCredentials(c clientCertCredentialConfig) (string, bool, error) { k8sConfigAccess := r.commonConfig.GetConfigAccess() // Store client certificate credential either into the current kubeconfig or a self-contained file if a path is given. - if r.loginOptions.selfContainedClientCert && c.filePath != "" { + if r.loginOptions.selfContainedWC && c.filePath != "" { return printWCClientCertCredentials(k8sConfigAccess, r.fs, c, r.loginOptions.contextOverride) } - return storeWCClientCertCredentials(k8sConfigAccess, r.fs, c, r.loginOptions.contextOverride) + return storeWCClientCertCredentials(k8sConfigAccess, c, r.loginOptions.contextOverride) +} + +func (r *runner) storeAWSIAMCredentials(c eksClusterConfig) (string, bool, error) { + k8sConfigAccess := r.commonConfig.GetConfigAccess() + if r.loginOptions.selfContained && c.filePath != "" { + return printWCAWSIamCredentials(k8sConfigAccess, r.fs, c, r.loginOptions.contextOverride) + } + return storeWCAWSIAMKubeconfig(k8sConfigAccess, c, r.loginOptions.contextOverride) } func getWCBasePath(k8sConfigAccess clientcmd.ConfigAccess, provider string, currentContext string) (string, error) { @@ -330,3 +392,7 @@ func getWCBasePath(k8sConfigAccess clientcmd.ConfigAccess, provider string, curr return strings.TrimPrefix(clusterServer, "g8s."), nil } + +func isEKS(c *capi.Cluster) bool { + return c.Spec.InfrastructureRef != nil && c.Spec.InfrastructureRef.Kind == "AWSManagedCluster" +} diff --git a/cmd/login/wc_test.go b/cmd/login/wc_test.go index 5d25399ab..3f4718a50 100644 --- a/cmd/login/wc_test.go +++ b/cmd/login/wc_test.go @@ -360,7 +360,7 @@ func TestWCClientCert(t *testing.T) { go createSecret(ctx, client, tc.provider) } - _, _, err = r.createClusterClientCert(ctx, client, tc.provider) + _, _, err = r.createClusterKubeconfig(ctx, client, tc.provider) if err != nil { if microerror.Cause(err) != tc.expectError { t.Fatalf("unexpected error: %s", err.Error()) diff --git a/go.mod b/go.mod index 2e301eb0f..b36e2ce13 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,8 @@ require ( k8s.io/cli-runtime v0.25.0 k8s.io/client-go v0.25.0 k8s.io/utils v0.0.0-20230726121419-3b25d923346b - sigs.k8s.io/cluster-api v1.1.4 + sigs.k8s.io/cluster-api v1.1.5 + sigs.k8s.io/cluster-api-provider-aws v1.5.5 sigs.k8s.io/cluster-api-provider-azure v1.3.2 sigs.k8s.io/controller-runtime v0.13.1 sigs.k8s.io/yaml v1.3.0 @@ -67,6 +68,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.44.319 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index 750fc56be..aadcf8c46 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -121,6 +122,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -320,6 +323,7 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobuffalo/flect v0.2.3/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= +github.com/gobuffalo/flect v0.2.5 h1:H6vvsv2an0lalEaCDRThvtBfmg44W/QHXBCYUXf/6S4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -715,8 +719,8 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -1378,6 +1382,8 @@ rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/cluster-api v1.0.4 h1:3vMvQjEmkSQayN9r0DXlfn8uWHzqNT+HPBveVryJ1Dc= sigs.k8s.io/cluster-api v1.0.4/go.mod h1:/LkJXtsvhxTV4U0z1Y2Y1Gr2xebJ0/ce09Ab2M0XU/U= +sigs.k8s.io/cluster-api-provider-aws v1.5.5 h1:+HbGNfw5OV1ifUOTs8aCk2ujiLtCt+jMirjrWtUUQbs= +sigs.k8s.io/cluster-api-provider-aws v1.5.5/go.mod h1:67wutYFBvXPdpBOs0JJpmLUMbf+1ay5IZ5m+Hdey3cA= sigs.k8s.io/cluster-api-provider-azure v1.3.2 h1:6lo7FTWpXeG6dAkBVkOPNxdaWOsQj2/4WgCwsZN4xQA= sigs.k8s.io/cluster-api-provider-azure v1.3.2/go.mod h1:VkdQF4idHrYgjpCR/ygc7pgLXA71ek8dQi2t47qqH/0= sigs.k8s.io/controller-runtime v0.10.3/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= diff --git a/pkg/kubeconfig/context.go b/pkg/kubeconfig/context.go index 799f8b8cf..33f28cc64 100644 --- a/pkg/kubeconfig/context.go +++ b/pkg/kubeconfig/context.go @@ -9,6 +9,7 @@ import ( const ( ContextPrefix = "gs-" ClientCertSuffix = "-clientcert" + AWSIAMSuffix = "-awsiam" ) type ContextType int @@ -35,6 +36,10 @@ func GenerateWCClientCertKubeContextName(mcKubeContextName string, wcName string return fmt.Sprintf("%s-%s%s", mcKubeContextName, wcName, ClientCertSuffix) } +func GenerateWCAWSIAMKubeContextName(mcKubeContextName string, wcName string) string { + return fmt.Sprintf("%s-%s%s", mcKubeContextName, wcName, AWSIAMSuffix) +} + // IsKubeContext checks whether the name provided, // matches our pattern for naming kubernetes contexts. func IsKubeContext(s string) (bool, ContextType) { diff --git a/pkg/scheme/scheme.go b/pkg/scheme/scheme.go index 8430bbb04..8ab31c5e0 100644 --- a/pkg/scheme/scheme.go +++ b/pkg/scheme/scheme.go @@ -11,6 +11,7 @@ import ( k8score "k8s.io/api/core/v1" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" + eks "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1" capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" capzexp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1" capi "sigs.k8s.io/cluster-api/api/v1beta1" @@ -23,6 +24,7 @@ func NewSchemeBuilder() []func(*runtime.Scheme) error { application.AddToScheme, // App, Catalog capi.AddToScheme, // Cluster capiexp.AddToScheme, // AWSMachinePool + eks.AddToScheme, // EKS CRs k8score.AddToScheme, // Secret, ConfigMap infrastructure.AddToScheme, // AWSCluster (Giant Swarm CAPI) capz.AddToScheme, // AzureCluster