Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

login-add-eks-cluster-support #1102

Merged
merged 11 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading