Skip to content

Commit

Permalink
login-add-eks-cluster-support (#1102)
Browse files Browse the repository at this point in the history
  • Loading branch information
calvix committed Aug 16, 2023
1 parent 1925430 commit 452438a
Show file tree
Hide file tree
Showing 13 changed files with 391 additions and 58 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
228 changes: 228 additions & 0 deletions cmd/login/aws.go
Original file line number Diff line number Diff line change
@@ -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"}
}
53 changes: 32 additions & 21 deletions cmd/login/clientcert.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type serviceSet struct {
releaseService release.Interface
}

type credentialConfig struct {
type clientCertCredentialConfig struct {
clusterID string
certCRT []byte
certKey []byte
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions cmd/login/clientcert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down
Loading

0 comments on commit 452438a

Please sign in to comment.