diff --git a/pkg/kubefedctl/join.go b/pkg/kubefedctl/join.go index a662504dbf..b7b8f142d8 100644 --- a/pkg/kubefedctl/join.go +++ b/pkg/kubefedctl/join.go @@ -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" @@ -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.") @@ -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 } @@ -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 } @@ -206,10 +207,10 @@ 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) } @@ -217,7 +218,7 @@ func JoinCluster(hostConfig, clusterConfig *rest.Config, kubefedNamespace, // 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() @@ -254,7 +255,7 @@ 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 { @@ -262,7 +263,8 @@ func joinClusterForNamespace(hostConfig, clusterConfig *rest.Config, kubefedName } 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 @@ -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, @@ -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) @@ -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 @@ -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 { @@ -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{ { @@ -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") @@ -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 } diff --git a/pkg/kubefedctl/util/util.go b/pkg/kubefedctl/util/util.go index 9655b7f05d..6c45d051f7 100644 --- a/pkg/kubefedctl/util/util.go +++ b/pkg/kubefedctl/util/util.go @@ -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.