Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

fix: Use specific service account token secret for join #1515

Merged
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
112 changes: 78 additions & 34 deletions pkg/kubefedctl/join.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
kubeclient "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"

fedv1b1 "sigs.k8s.io/kubefed/pkg/apis/core/v1beta1"
Expand Down Expand Up @@ -93,15 +94,15 @@ type joinFederation struct {
}

type joinFederationOptions struct {
secretName string
scope apiextv1.ResourceScope
errorOnExisting bool
hostClusterSecretName string
scope apiextv1.ResourceScope
errorOnExisting bool
}

// Bind adds the join specific arguments to the flagset passed in as an
// argument.
func (o *joinFederationOptions) Bind(flags *pflag.FlagSet) {
flags.StringVar(&o.secretName, "secret-name", "",
flags.StringVar(&o.hostClusterSecretName, "secret-name", "",
"Name of the secret where the cluster's credentials will be stored in the host cluster. This name should be a valid RFC 1035 label. If unspecified, defaults to a generated name containing the cluster name.")
flags.BoolVar(&o.errorOnExisting, "error-on-existing", true,
"Whether the join operation will throw an error if it encounters existing artifacts with the same name as those it's trying to create. If false, the join operation will update existing artifacts to match its own specification.")
Expand Down Expand Up @@ -160,7 +161,7 @@ func (j *joinFederation) Complete(args []string) error {

klog.V(2).Infof("Args and flags: name %s, host: %s, host-system-namespace: %s, kubeconfig: %s, cluster-context: %s, secret-name: %s, dry-run: %v",
j.ClusterName, j.HostClusterContext, j.KubeFedNamespace, j.Kubeconfig, j.ClusterContext,
j.secretName, j.DryRun)
j.hostClusterSecretName, j.DryRun)

return nil
}
Expand Down Expand Up @@ -197,7 +198,7 @@ func (j *joinFederation) Run(cmdOut io.Writer, config util.FedConfig) error {
}

_, err = JoinCluster(hostConfig, clusterConfig, j.KubeFedNamespace,
hostClusterName, j.ClusterName, j.secretName, j.joinFederationOptions.scope, j.DryRun, j.errorOnExisting)
hostClusterName, j.ClusterName, j.hostClusterSecretName, j.joinFederationOptions.scope, j.DryRun, j.errorOnExisting)

return err
}
Expand All @@ -206,18 +207,18 @@ func (j *joinFederation) Run(cmdOut io.Writer, config util.FedConfig) error {
// KubeFed namespace in the joining cluster will be the same as in the
// host cluster.
func JoinCluster(hostConfig, clusterConfig *rest.Config, kubefedNamespace,
hostClusterName, joiningClusterName, secretName string,
hostClusterName, joiningClusterName, hostClusterSecretName string,
scope apiextv1.ResourceScope, dryRun, errorOnExisting bool) (*fedv1b1.KubeFedCluster, error) {
return joinClusterForNamespace(hostConfig, clusterConfig, kubefedNamespace,
kubefedNamespace, hostClusterName, joiningClusterName, secretName,
kubefedNamespace, hostClusterName, joiningClusterName, hostClusterSecretName,
scope, dryRun, errorOnExisting)
}

// joinClusterForNamespace registers a cluster with a KubeFed control
// plane. The KubeFed namespace in the joining cluster is provided by
// the joiningNamespace parameter.
func joinClusterForNamespace(hostConfig, clusterConfig *rest.Config, kubefedNamespace,
joiningNamespace, hostClusterName, joiningClusterName, secretName string,
joiningNamespace, hostClusterName, joiningClusterName, hostClusterSecretName string,
scope apiextv1.ResourceScope, dryRun, errorOnExisting bool) (*fedv1b1.KubeFedCluster, error) {
start := time.Now()

Expand Down Expand Up @@ -254,15 +255,16 @@ func joinClusterForNamespace(hostConfig, clusterConfig *rest.Config, kubefedName
}
klog.V(2).Infof("Created %s namespace in joining cluster", joiningNamespace)

saName, err := createAuthorizedServiceAccount(clusterClientset,
joiningClusterSATokenSecretName, err := createAuthorizedServiceAccount(clusterClientset,
joiningNamespace, joiningClusterName, hostClusterName,
scope, dryRun, errorOnExisting)
if err != nil {
return nil, err
}

secret, caBundle, err := populateSecretInHostCluster(clusterClientset, hostClientset,
saName, kubefedNamespace, joiningNamespace, joiningClusterName, secretName, dryRun, errorOnExisting)
joiningClusterSATokenSecretName, kubefedNamespace, joiningNamespace, joiningClusterName,
hostClusterSecretName, dryRun, errorOnExisting)
if err != nil {
klog.V(2).Infof("Error creating secret in host cluster: %s due to: %v", hostClusterName, err)
return nil, err
Expand Down Expand Up @@ -417,13 +419,13 @@ func createKubeFedNamespace(clusterClientset kubeclient.Interface, kubefedNamesp
return fedNamespace, nil
}

// createAuthorizedServiceAccount creates a service account and grants
// the privileges required by the KubeFed control plane to manage
// createAuthorizedServiceAccount creates a service account and service account token secret
// and grants the privileges required by the KubeFed control plane to manage
// resources in the joining cluster. The name of the created service
// account is returned on success.
func createAuthorizedServiceAccount(joiningClusterClientset kubeclient.Interface,
namespace, joiningClusterName, hostClusterName string,
scope apiextv1.ResourceScope, dryRun, errorOnExisting bool) (string, error) {
scope apiextv1.ResourceScope, dryRun, errorOnExisting bool) (saTokenSecretName string, err error) {
klog.V(2).Infof("Creating service account in joining cluster: %s", joiningClusterName)

saName, err := createServiceAccount(joiningClusterClientset, namespace,
Expand All @@ -436,6 +438,16 @@ func createAuthorizedServiceAccount(joiningClusterClientset kubeclient.Interface

klog.V(2).Infof("Created service account: %s in joining cluster: %s", saName, joiningClusterName)

saTokenSecretName, err = createServiceAccountTokenSecret(saName, joiningClusterClientset, namespace,
joiningClusterName, hostClusterName, dryRun, errorOnExisting)
if err != nil {
klog.V(2).Infof("Error creating service account token secret: %s in joining cluster: %s due to: %v",
saName, joiningClusterName, err)
return "", err
}

klog.V(2).Infof("Created service account token secret: %s in joining cluster: %s", saTokenSecretName, joiningClusterName)

if scope == apiextv1.NamespaceScoped {
klog.V(2).Infof("Creating role and binding for service account: %s in joining cluster: %s", saName, joiningClusterName)

Expand Down Expand Up @@ -474,7 +486,7 @@ func createAuthorizedServiceAccount(joiningClusterClientset kubeclient.Interface
saName, joiningClusterName)
}

return saName, nil
return saTokenSecretName, nil
}

// createServiceAccount creates a service account in the cluster associated
Expand All @@ -487,7 +499,11 @@ func createServiceAccount(clusterClientset kubeclient.Interface, namespace,
ObjectMeta: metav1.ObjectMeta{
Name: saName,
Namespace: namespace,
Annotations: map[string]string{
"kubernetes.io/enforce-mountable-secrets": "true",
},
},
AutomountServiceAccountToken: pointer.Bool(false),
}

if dryRun {
Expand All @@ -510,6 +526,45 @@ func createServiceAccount(clusterClientset kubeclient.Interface, namespace,
}
}

// createServiceAccountTokenSecret creates a service account token secret in the cluster associated
// with clusterClientset with credentials that will be used by the host cluster
// to access its API server.
func createServiceAccountTokenSecret(saName string, clusterClientset kubeclient.Interface, namespace,
joiningClusterName, hostClusterName string, dryRun, errorOnExisting bool) (string, error) {
saTokenSecretName := util.ClusterServiceAccountTokenSecretName(joiningClusterName, hostClusterName)
saTokenSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: saTokenSecretName,
Namespace: namespace,
Annotations: map[string]string{
"kubernetes.io/service-account.name": saName,
},
},
Type: corev1.SecretTypeServiceAccountToken,
}

if dryRun {
return saName, nil
}

// Create a new service account.
_, err := clusterClientset.CoreV1().Secrets(namespace).Create(
context.Background(), saTokenSecret, metav1.CreateOptions{},
)
switch {
case apierrors.IsAlreadyExists(err) && errorOnExisting:
klog.V(2).Infof("Service account token secret %s/%s already exists in target cluster %s",
namespace, saName, joiningClusterName)
return "", err
case err != nil && !apierrors.IsAlreadyExists(err):
klog.V(2).Infof("Could not create service account token secret %s/%s in target cluster %s due to: %v",
namespace, saName, joiningClusterName, err)
return "", err
default:
return saTokenSecretName, nil
}
}

func bindingSubjects(saName, namespace string) []rbacv1.Subject {
return []rbacv1.Subject{
{
Expand Down Expand Up @@ -828,7 +883,7 @@ func createHealthCheckClusterRoleAndBinding(clientset kubeclient.Interface, saNa
// hostClientset, putting it in a secret named secretName in the provided
// namespace.
func populateSecretInHostCluster(clusterClientset, hostClientset kubeclient.Interface,
saName, hostNamespace, joiningNamespace, joiningClusterName, secretName string,
saTokenSecretName, hostNamespace, joiningNamespace, joiningClusterName, secretName string,
dryRun bool, errorOnExisting bool) (*corev1.Secret, []byte, error) {
klog.V(2).Infof("Creating cluster credentials secret in host cluster")

Expand All @@ -840,33 +895,22 @@ func populateSecretInHostCluster(clusterClientset, hostClientset kubeclient.Inte

// Get the secret from the joining cluster.
var secret *corev1.Secret

err := wait.PollImmediate(1*time.Second, serviceAccountSecretTimeout, func() (bool, error) {
sa, err := clusterClientset.CoreV1().ServiceAccounts(joiningNamespace).Get(
context.Background(), saName, metav1.GetOptions{},
joiningClusterSASecret, err := clusterClientset.CoreV1().Secrets(joiningNamespace).Get(
context.Background(), saTokenSecretName, metav1.GetOptions{},
)
if err != nil {
return false, nil
}

for _, objReference := range sa.Secrets {
saSecretName := objReference.Name
var err error
secret, err = clusterClientset.CoreV1().Secrets(joiningNamespace).Get(
context.Background(), saSecretName, metav1.GetOptions{},
)
if err != nil {
return false, nil
}
if secret.Type == corev1.SecretTypeServiceAccountToken {
klog.V(2).Infof("Using secret named: %s", secret.Name)
return true, nil
}
}
return false, nil
secret = joiningClusterSASecret

return true, nil
})

if err != nil {
klog.V(2).Infof("Could not get service account secret from joining cluster: %v", err)
klog.V(2).Infof("Could not get service account token secret from joining cluster: %v", err)
return nil, nil, err
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/kubefedctl/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ func ClusterServiceAccountName(joiningClusterName, hostClusterName string) strin
return fmt.Sprintf("%s-%s", joiningClusterName, hostClusterName)
}

// ClusterServiceAccountTokenSecretName returns the name of a service account token secret whose
// credentials are used by the host cluster to access the client cluster.
func ClusterServiceAccountTokenSecretName(joiningClusterName, hostClusterName string) string {
return fmt.Sprintf("%s-%s", joiningClusterName, hostClusterName)
}

// RoleName returns the name of a Role or ClusterRole and its
// associated RoleBinding or ClusterRoleBinding that are used to allow
// the service account to access necessary resources on the cluster.
Expand Down