Commit f30443ca authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #42042 from perotinus/svcaccounts

Automatic merge from submit-queue (batch tested with PRs 42042, 46139, 46126, 46258, 46312) [Federation] Use service accounts instead of the user's credentials when accessing joined clusters' API servers. Fixes #41267. Release notes: ```release-note Modifies kubefed to create and the federation controller manager to use credentials associated with a service account rather than the user's credentials. ```
parents 90250220 af2a8f7e
......@@ -47,6 +47,7 @@ go_test(
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_clientset:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/testapi:go_default_library",
"//pkg/api/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
......
......@@ -31,6 +31,7 @@ import (
federationv1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
federationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_clientset"
controllerutil "k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/v1"
)
......@@ -124,9 +125,9 @@ func TestUpdateClusterStatusOK(t *testing.T) {
}
federationClientSet := federationclientset.NewForConfigOrDie(restclient.AddUserAgent(restClientCfg, "cluster-controller"))
// Override KubeconfigGetterForCluster to avoid having to setup service accounts and mount files with secret tokens.
originalGetter := controllerutil.KubeconfigGetterForCluster
controllerutil.KubeconfigGetterForCluster = func(c *federationv1beta1.Cluster) clientcmd.KubeconfigGetter {
// Override KubeconfigGetterForSecret to avoid having to setup service accounts and mount files with secret tokens.
originalGetter := controllerutil.KubeconfigGetterForSecret
controllerutil.KubeconfigGetterForSecret = func(s *api.Secret) clientcmd.KubeconfigGetter {
return func() (*clientcmdapi.Config, error) {
return &clientcmdapi.Config{}, nil
}
......@@ -146,6 +147,6 @@ func TestUpdateClusterStatusOK(t *testing.T) {
}
}
// Reset KubeconfigGetterForCluster
controllerutil.KubeconfigGetterForCluster = originalGetter
// Reset KubeconfigGetterForSecret
controllerutil.KubeconfigGetterForSecret = originalGetter
}
......@@ -65,8 +65,29 @@ func BuildClusterConfig(c *federation_v1beta1.Cluster) (*restclient.Config, erro
glog.Infof("didn't find secretRef for cluster %s. Trying insecure access", c.Name)
clusterConfig, err = clientcmd.BuildConfigFromFlags(serverAddress, "")
} else {
kubeconfigGetter := KubeconfigGetterForCluster(c)
clusterConfig, err = clientcmd.BuildConfigFromKubeconfigGetter(serverAddress, kubeconfigGetter)
if c.Spec.SecretRef.Name == "" {
return nil, fmt.Errorf("found secretRef but no secret name for cluster %s", c.Name)
}
secret, err := getSecret(c.Spec.SecretRef.Name)
if err != nil {
return nil, err
}
// Pre-1.7, the secret contained a serialized kubeconfig which contained appropriate credentials.
// Post-1.7, the secret contains credentials for a service account.
// Check for the service account credentials, and use them if they exist; if not, use the
// serialized kubeconfig.
token, tokenFound := secret.Data["token"]
ca, caFound := secret.Data["ca.crt"]
if tokenFound != caFound {
return nil, fmt.Errorf("secret should have values for either both 'ca.crt' and 'token' in its Data, or neither: %v", secret)
} else if tokenFound && caFound {
clusterConfig, err = clientcmd.BuildConfigFromFlags(serverAddress, "")
clusterConfig.CAData = ca
clusterConfig.BearerToken = string(token)
} else {
kubeconfigGetter := KubeconfigGetterForSecret(secret)
clusterConfig, err = clientcmd.BuildConfigFromKubeconfigGetter(serverAddress, kubeconfigGetter)
}
}
if err != nil {
return nil, err
......@@ -77,60 +98,49 @@ func BuildClusterConfig(c *federation_v1beta1.Cluster) (*restclient.Config, erro
return clusterConfig, nil
}
// This is to inject a different kubeconfigGetter in tests.
// We don't use the standard one which calls NewInCluster in tests to avoid having to setup service accounts and mount files with secret tokens.
var KubeconfigGetterForCluster = func(c *federation_v1beta1.Cluster) clientcmd.KubeconfigGetter {
return func() (*clientcmdapi.Config, error) {
secretRefName := ""
if c.Spec.SecretRef != nil {
secretRefName = c.Spec.SecretRef.Name
} else {
glog.Infof("didn't find secretRef for cluster %s. Trying insecure access", c.Name)
// getSecret gets a secret from the cluster.
func getSecret(secretName string) (*api.Secret, error) {
// Get the namespace this is running in from the env variable.
namespace := os.Getenv("POD_NAMESPACE")
if namespace == "" {
return nil, fmt.Errorf("unexpected: POD_NAMESPACE env var returned empty string")
}
// Get a client to talk to the k8s apiserver, to fetch secrets from it.
cc, err := restclient.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("error in creating in-cluster config: %s", err)
}
client, err := clientset.NewForConfig(cc)
if err != nil {
return nil, fmt.Errorf("error in creating in-cluster client: %s", err)
}
var secret *api.Secret
err = wait.PollImmediate(1*time.Second, getSecretTimeout, func() (bool, error) {
secret, err = client.Core().Secrets(namespace).Get(secretName, metav1.GetOptions{})
if err == nil {
return true, nil
}
return KubeconfigGetterForSecret(secretRefName)()
glog.Warningf("error in fetching secret: %s", err)
return false, nil
})
if err != nil {
return nil, fmt.Errorf("timed out waiting for secret: %s", err)
}
if secret == nil {
return nil, fmt.Errorf("unexpected: received null secret %s", secretName)
}
return secret, nil
}
// KubeconfigGetterForSecret is used to get the kubeconfig from the given secret.
var KubeconfigGetterForSecret = func(secretName string) clientcmd.KubeconfigGetter {
// KubeconfigGetterForSecret gets the kubeconfig from the given secret.
// This is to inject a different KubeconfigGetter in tests. We don't use
// the standard one which calls NewInCluster in tests to avoid having to
// set up service accounts and mount files with secret tokens.
var KubeconfigGetterForSecret = func(secret *api.Secret) clientcmd.KubeconfigGetter {
return func() (*clientcmdapi.Config, error) {
var data []byte
if secretName != "" {
// Get the namespace this is running in from the env variable.
namespace := os.Getenv("POD_NAMESPACE")
if namespace == "" {
return nil, fmt.Errorf("unexpected: POD_NAMESPACE env var returned empty string")
}
// Get a client to talk to the k8s apiserver, to fetch secrets from it.
cc, err := restclient.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("error in creating in-cluster client: %s", err)
}
client, err := clientset.NewForConfig(cc)
if err != nil {
return nil, fmt.Errorf("error in creating in-cluster client: %s", err)
}
data = []byte{}
var secret *api.Secret
err = wait.PollImmediate(1*time.Second, getSecretTimeout, func() (bool, error) {
secret, err = client.Core().Secrets(namespace).Get(secretName, metav1.GetOptions{})
if err == nil {
return true, nil
}
glog.Warningf("error in fetching secret: %s", err)
return false, nil
})
if err != nil {
return nil, fmt.Errorf("timed out waiting for secret: %s", err)
}
if secret == nil {
return nil, fmt.Errorf("unexpected: received null secret %s", secretName)
}
ok := false
data, ok = secret.Data[KubeconfigSecretDataKey]
if !ok {
return nil, fmt.Errorf("secret does not have data with key: %s", KubeconfigSecretDataKey)
}
data, ok := secret.Data[KubeconfigSecretDataKey]
if !ok {
return nil, fmt.Errorf("secret does not have data with key %s", KubeconfigSecretDataKey)
}
return clientcmd.Load(data)
}
......
......@@ -23,6 +23,7 @@ go_library(
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//pkg/apis/rbac:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd:go_default_library",
......@@ -36,6 +37,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/flag:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
......@@ -59,6 +61,8 @@ go_test(
"//pkg/api/testapi:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/apis/rbac/v1beta1:go_default_library",
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd/testing:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
......
......@@ -29,11 +29,13 @@ import (
"k8s.io/kubernetes/federation/pkg/kubefed/util"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
......@@ -47,11 +49,20 @@ var (
# Federation control plane's host cluster context name
# must be specified via the --host-cluster-context flag
# to properly cleanup the credentials.
kubectl unjoin foo --host-cluster-context=bar`)
kubectl unjoin foo --host-cluster-context=bar --cluster-context=baz`)
)
type unjoinFederation struct {
commonOptions util.SubcommandOptions
options unjoinFederationOptions
}
type unjoinFederationOptions struct {
clusterContext string
}
func (o *unjoinFederationOptions) Bind(flags *pflag.FlagSet) {
flags.StringVar(&o.clusterContext, "cluster-context", "", "Name of the cluster's context in the local kubeconfig. Defaults to cluster name if unspecified.")
}
// NewCmdUnjoin defines the `unjoin` command that removes a cluster
......@@ -72,12 +83,17 @@ func NewCmdUnjoin(f cmdutil.Factory, cmdOut, cmdErr io.Writer, config util.Admin
flags := cmd.Flags()
opts.commonOptions.Bind(flags)
opts.options.Bind(flags)
return cmd
}
// unjoinFederation is the implementation of the `unjoin` command.
func (u *unjoinFederation) Run(f cmdutil.Factory, cmdOut, cmdErr io.Writer, config util.AdminConfig) error {
if u.options.clusterContext == "" {
u.options.clusterContext = u.commonOptions.Name
}
cluster, err := popCluster(f, u.commonOptions.Name)
if err != nil {
return err
......@@ -101,26 +117,53 @@ func (u *unjoinFederation) Run(f cmdutil.Factory, cmdOut, cmdErr io.Writer, conf
// If this is the case, we cannot get the cluster clientset to delete the
// config map from that cluster and obviously cannot delete the not existing secret.
// We just publish the warning as cluster has already been removed from federation.
fmt.Fprintf(cmdErr, "WARNING: secret %q not found in the host cluster, so it couldn't be deleted", secretName)
fmt.Fprintf(cmdErr, "WARNING: secret %q not found in the host cluster, so it couldn't be deleted. Cluster has already been removed from the federation.", secretName)
return nil
} else if err != nil {
fmt.Fprintf(cmdErr, "WARNING: Error retrieving secret from the base cluster")
} else {
err := deleteSecret(hostClientset, cluster.Spec.SecretRef.Name, u.commonOptions.FederationSystemNamespace)
if err != nil {
fmt.Fprintf(cmdErr, "WARNING: secret %q could not be deleted: %v", secretName, err)
// We anyways continue to try and delete the config map but with above warning
}
return err
}
// We need to ensure updating the config map created in the deregistered cluster
// This configmap was created/updated when the cluster joined this federation to aid
// the kube-dns of that cluster to aid service discovery.
err = updateConfigMapFromCluster(hostClientset, secret, cluster, u.commonOptions.FederationSystemNamespace)
unjoiningClusterFactory := config.ClusterFactory(u.options.clusterContext, u.commonOptions.Kubeconfig)
unjoiningClusterClientset, err := unjoiningClusterFactory.ClientSet()
outerErr := err
if err != nil {
// Attempt to get a clientset using information from the cluster.
unjoiningClusterClientset, err = getClientsetFromCluster(secret, cluster)
if err != nil {
fmt.Fprintf(cmdErr, "WARNING: Encountered error in deleting kube-dns configmap, %v", err)
// We anyways continue to print success message but with above warning
return fmt.Errorf("unable to get clientset from kubeconfig or cluster: %v, %v", outerErr, err)
}
}
err = deleteSecret(hostClientset, cluster.Spec.SecretRef.Name, u.commonOptions.FederationSystemNamespace)
if err != nil {
fmt.Fprintf(cmdErr, "WARNING: secret %q could not be deleted: %v", secretName, err)
// We anyways continue to try and delete the config map but with above warning
}
// We need to ensure updating the config map created in the deregistered cluster
// This configmap was created/updated when the cluster joined this federation to aid
// the kube-dns of that cluster to aid service discovery.
err = updateConfigMapFromCluster(hostClientset, unjoiningClusterClientset, u.commonOptions.FederationSystemNamespace)
if err != nil {
fmt.Fprintf(cmdErr, "WARNING: Encountered error in deleting kube-dns configmap: %v", err)
// We anyways continue to print success message but with above warning
}
// Delete the service account in the unjoining cluster.
err = deleteServiceAccountFromCluster(unjoiningClusterClientset, cluster, u.commonOptions.FederationSystemNamespace)
if err != nil {
fmt.Fprintf(cmdErr, "WARNING: Encountered error in deleting service account: %v", err)
return err
}
// Delete the cluster role and role binding in the unjoining cluster.
err = deleteClusterRoleBindingFromCluster(unjoiningClusterClientset, cluster)
if err != nil {
fmt.Fprintf(cmdErr, "WARNING: Encountered error in deleting cluster role bindings: %v", err)
return err
}
_, err = fmt.Fprintf(cmdOut, "Successfully removed cluster %q from federation\n", u.commonOptions.Name)
return err
}
......@@ -162,12 +205,7 @@ func popCluster(f cmdutil.Factory, name string) (*federationapi.Cluster, error)
return cluster, rh.Delete("", name)
}
func updateConfigMapFromCluster(hostClientset internalclientset.Interface, secret *api.Secret, cluster *federationapi.Cluster, fedSystemNamespace string) error {
clientset, err := getClientsetFromCluster(secret, cluster)
if err != nil {
return err
}
func updateConfigMapFromCluster(hostClientset, unjoiningClusterClientset internalclientset.Interface, fedSystemNamespace string) error {
cmDep, err := getCMDeployment(hostClientset, fedSystemNamespace)
if err != nil {
return err
......@@ -177,7 +215,7 @@ func updateConfigMapFromCluster(hostClientset internalclientset.Interface, secre
return fmt.Errorf("kube-dns config map data missing from controller manager annotations")
}
configMap, err := clientset.Core().ConfigMaps(metav1.NamespaceSystem).Get(util.KubeDnsConfigmapName, metav1.GetOptions{})
configMap, err := unjoiningClusterClientset.Core().ConfigMaps(metav1.NamespaceSystem).Get(util.KubeDnsConfigmapName, metav1.GetOptions{})
if err != nil {
return err
}
......@@ -194,7 +232,7 @@ func updateConfigMapFromCluster(hostClientset internalclientset.Interface, secre
}
if needUpdate {
_, err = clientset.Core().ConfigMaps(metav1.NamespaceSystem).Update(configMap)
_, err = unjoiningClusterClientset.Core().ConfigMaps(metav1.NamespaceSystem).Update(configMap)
}
return err
}
......@@ -262,3 +300,34 @@ func removeConfigMapString(str string, toRemove string) string {
}
return strings.Join(values, ",")
}
// deleteServiceAccountFromCluster removes the service account that the federation control plane uses
// to access the cluster from the cluster that is leaving the federation.
func deleteServiceAccountFromCluster(unjoiningClusterClientset internalclientset.Interface, cluster *federationapi.Cluster, fedSystemNamespace string) error {
serviceAccountName, ok := cluster.ObjectMeta.Annotations[kubectl.ServiceAccountNameAnnotation]
if !ok {
// If there is no service account name annotation, assume that this cluster does not have a federation control plane service account.
return nil
}
return unjoiningClusterClientset.Core().ServiceAccounts(fedSystemNamespace).Delete(serviceAccountName, &metav1.DeleteOptions{})
}
// deleteClusterRoleBindingFromCluster deletes the ClusterRole and ClusterRoleBinding from the
// cluster that is leaving the federation.
func deleteClusterRoleBindingFromCluster(unjoiningClusterClientset internalclientset.Interface, cluster *federationapi.Cluster) error {
clusterRoleName, ok := cluster.ObjectMeta.Annotations[kubectl.ClusterRoleNameAnnotation]
if !ok {
// If there is no cluster role name annotation, assume that this cluster does not have cluster role bindings.
return nil
}
err := unjoiningClusterClientset.Rbac().ClusterRoleBindings().Delete(clusterRoleName, &metav1.DeleteOptions{})
if err != nil && !errors.IsMethodNotSupported(err) && !errors.IsNotFound(err) {
return err
}
err = unjoiningClusterClientset.Rbac().ClusterRoles().Delete(clusterRoleName, &metav1.DeleteOptions{})
if err != nil && !errors.IsMethodNotSupported(err) && !errors.IsNotFound(err) {
return err
}
return nil
}
......@@ -119,7 +119,7 @@ func TestUnjoinFederation(t *testing.T) {
kubeconfigGlobal: fakeKubeFiles[0],
kubeconfigExplicit: "",
expectedServer: "https://10.20.30.40",
expectedErr: fmt.Sprintf("WARNING: secret %q not found in the host cluster, so it couldn't be deleted", "noexist"),
expectedErr: fmt.Sprintf("WARNING: secret %q not found in the host cluster, so it couldn't be deleted. Cluster has already been removed from the federation.", "noexist"),
},
// TODO: Figure out a way to test the scenarios of configmap deletion
// As of now we delete the config map after deriving the clientset using
......@@ -171,7 +171,7 @@ func TestUnjoinFederation(t *testing.T) {
func testUnjoinFederationFactory(name, server, secret string) cmdutil.Factory {
urlPrefix := "/clusters/"
cluster := fakeCluster(name, name, server)
cluster := fakeCluster(name, name, server, true)
if secret != "" {
cluster.Spec.SecretRef.Name = secret
}
......@@ -212,8 +212,11 @@ func testUnjoinFederationFactory(name, server, secret string) cmdutil.Factory {
return f
}
func fakeUnjoinHostFactory(name string) cmdutil.Factory {
urlPrefix := "/api/v1/namespaces/federation-system/secrets/"
func fakeUnjoinHostFactory(clusterName string) cmdutil.Factory {
secretsPrefix := "/api/v1/namespaces/federation-system/secrets/"
clusterRolePrefix := "/apis/rbac.authorization.k8s.io/v1beta1/clusterroles/"
serviceAccountPrefix := "/api/v1/namespaces/federation-system/serviceaccounts/"
clusterRoleBindingPrefix := "/apis/rbac.authorization.k8s.io/v1beta1/clusterrolebindings/"
// Using dummy bytes for now
configBytes, _ := clientcmd.Write(clientcmdapi.Config{})
......@@ -223,7 +226,7 @@ func fakeUnjoinHostFactory(name string) cmdutil.Factory {
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Name: clusterName,
Namespace: util.DefaultFederationSystemNamespace,
},
Data: map[string][]byte{
......@@ -239,11 +242,11 @@ func fakeUnjoinHostFactory(name string) cmdutil.Factory {
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case strings.HasPrefix(p, urlPrefix):
case strings.HasPrefix(p, secretsPrefix):
switch m {
case http.MethodDelete:
got := strings.TrimPrefix(p, urlPrefix)
if got != name {
got := strings.TrimPrefix(p, secretsPrefix)
if got != clusterName {
return nil, errors.NewNotFound(api.Resource("secrets"), got)
}
status := metav1.Status{
......@@ -251,14 +254,47 @@ func fakeUnjoinHostFactory(name string) cmdutil.Factory {
}
return &http.Response{StatusCode: http.StatusOK, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &status)}, nil
case http.MethodGet:
got := strings.TrimPrefix(p, urlPrefix)
if got != name {
got := strings.TrimPrefix(p, secretsPrefix)
if got != clusterName {
return nil, errors.NewNotFound(api.Resource("secrets"), got)
}
return &http.Response{StatusCode: http.StatusOK, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &secretObject)}, nil
default:
return nil, fmt.Errorf("unexpected request method: %#v\n%#v", req.URL, req)
}
case strings.HasPrefix(p, serviceAccountPrefix) && m == http.MethodDelete:
got := strings.TrimPrefix(p, serviceAccountPrefix)
want := serviceAccountName(clusterName)
if got != want {
return nil, errors.NewNotFound(api.Resource("serviceaccounts"), got)
}
status := metav1.Status{
Status: "Success",
}
return &http.Response{StatusCode: http.StatusOK, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &status)}, nil
case strings.HasPrefix(p, clusterRoleBindingPrefix) && m == http.MethodDelete:
got := strings.TrimPrefix(p, clusterRoleBindingPrefix)
want := util.ClusterRoleName(serviceAccountName(clusterName))
if got != want {
return nil, errors.NewNotFound(api.Resource("clusterrolebindings"), got)
}
status := metav1.Status{
Status: "Success",
}
return &http.Response{StatusCode: http.StatusOK, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &status)}, nil
case strings.HasPrefix(p, clusterRolePrefix) && m == http.MethodDelete:
got := strings.TrimPrefix(p, clusterRolePrefix)
want := util.ClusterRoleName(serviceAccountName(clusterName))
if got != want {
return nil, errors.NewNotFound(api.Resource("clusterroles"), got)
}
status := metav1.Status{
Status: "Success",
}
return &http.Response{StatusCode: http.StatusOK, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &status)}, nil
default:
return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req)
}
......
......@@ -226,11 +226,29 @@ func GetServerAddress(c *federationapi.Cluster) (string, error) {
}
func buildConfigFromSecret(secret *api.Secret, serverAddress string) (*restclient.Config, error) {
kubeconfigGetter := kubeconfigGetterForSecret(secret)
clusterConfig, err := clientcmd.BuildConfigFromKubeconfigGetter(serverAddress, kubeconfigGetter)
var clusterConfig *restclient.Config
var err error
// Pre-1.7, the secret contained a serialized kubeconfig which contained appropriate credentials.
// Post-1.7, the secret contains credentials for a service account.
// Check for the service account credentials, and use them if they exist; if not, use the
// serialized kubeconfig.
token, tokenFound := secret.Data["token"]
ca, caFound := secret.Data["ca.crt"]
if tokenFound != caFound {
return nil, fmt.Errorf("secret should have values for either both 'ca.crt' and 'token' in its Data, or neither: %v", secret)
} else if tokenFound && caFound {
clusterConfig, err = clientcmd.BuildConfigFromFlags(serverAddress, "")
clusterConfig.CAData = ca
clusterConfig.BearerToken = string(token)
} else {
kubeconfigGetter := kubeconfigGetterForSecret(secret)
clusterConfig, err = clientcmd.BuildConfigFromKubeconfigGetter(serverAddress, kubeconfigGetter)
}
if err != nil {
return nil, err
}
clusterConfig.QPS = KubeAPIQPS
clusterConfig.Burst = KubeAPIBurst
......@@ -273,3 +291,17 @@ func GetVersionedClientForRBACOrFail(hostFactory cmdutil.Factory) (client.Interf
return nil, &NoRBACAPIError{rbacAPINotAvailable}
}
// ClusterServiceAccountName returns the name of a service account
// whose credentials are used by the host cluster to access the
// client cluster.
func ClusterServiceAccountName(joiningClusterName, hostContext string) string {
return fmt.Sprintf("%s-%s", joiningClusterName, hostContext)
}
// ClusterRoleName returns the name of a ClusterRole and its associated
// ClusterRoleBinding that are used to allow the service account to
// access necessary resources on the cluster.
func ClusterRoleName(serviceAccountName string) string {
return fmt.Sprintf("federation-controller-manager:%s", serviceAccountName)
}
......@@ -25,6 +25,11 @@ import (
"k8s.io/kubernetes/pkg/api/v1"
)
const (
ServiceAccountNameAnnotation = "federation.kubernetes.io/servive-account-name"
ClusterRoleNameAnnotation = "federation.kubernetes.io/cluster-role-name"
)
// ClusterGeneratorV1Beta1 supports stable generation of a
// federation/cluster resource.
type ClusterGeneratorV1Beta1 struct {
......@@ -39,6 +44,15 @@ type ClusterGeneratorV1Beta1 struct {
// SecretName is the name of the secret that stores the credentials
// for the Kubernetes cluster that is being registered (optional)
SecretName string
// ServiceAccountName is the name of the service account that is
// created in the cluster being registered. If this is provided,
// then ClusterRoleName must also be provided (optional)
ServiceAccountName string
// ClusterRoleName is the name of the cluster role and cluster role
// binding that are created in the cluster being registered. If this
// is provided, then ServiceAccountName must also be provided
// (optional)
ClusterRoleName string
}
// Ensure it supports the generator pattern that uses parameter
......@@ -68,6 +82,8 @@ func (s ClusterGeneratorV1Beta1) Generate(genericParams map[string]interface{})
clustergen.ClientCIDR = params["client-cidr"]
clustergen.ServerAddress = params["server-address"]
clustergen.SecretName = params["secret"]
clustergen.ServiceAccountName = params["service-account-name"]
clustergen.ClusterRoleName = params["cluster-role-name"]
return clustergen.StructuredGenerate()
}
......@@ -79,6 +95,8 @@ func (s ClusterGeneratorV1Beta1) ParamNames() []GeneratorParam {
{"client-cidr", false},
{"server-address", true},
{"secret", false},
{"service-account-name", false},
{"cluster-role-name", false},
}
}
......@@ -110,6 +128,21 @@ func (s ClusterGeneratorV1Beta1) StructuredGenerate() (runtime.Object, error) {
},
},
}
annotations := make(map[string]string)
if s.ServiceAccountName != "" {
annotations[ServiceAccountNameAnnotation] = s.ServiceAccountName
}
if s.ClusterRoleName != "" {
annotations[ClusterRoleNameAnnotation] = s.ClusterRoleName
}
if len(annotations) == 1 {
return nil, fmt.Errorf("Either both or neither of ServiceAccountName and ClusterRoleName must be provided.")
}
if len(annotations) > 0 {
cluster.SetAnnotations(annotations)
}
return cluster, nil
}
......
......@@ -108,6 +108,62 @@ func TestClusterGenerate(t *testing.T) {
},
{
params: map[string]interface{}{
"name": "bar-cluster",
"client-cidr": "10.20.30.40/16",
"server-address": "http://10.20.30.40",
"secret": "credentials",
},
expected: &federationapi.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "bar-cluster",
},
Spec: federationapi.ClusterSpec{
ServerAddressByClientCIDRs: []federationapi.ServerAddressByClientCIDR{
{
ClientCIDR: "10.20.30.40/16",
ServerAddress: "http://10.20.30.40",
},
},
SecretRef: &v1.LocalObjectReference{
Name: "credentials",
},
},
},
expectErr: false,
},
{
params: map[string]interface{}{
"name": "bar-cluster",
"client-cidr": "10.20.30.40/16",
"server-address": "http://10.20.30.40",
"secret": "credentials",
"service-account-name": "service-account",
"cluster-role-name": "cluster-role",
},
expected: &federationapi.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "bar-cluster",
Annotations: map[string]string{
ServiceAccountNameAnnotation: "service-account",
ClusterRoleNameAnnotation: "cluster-role",
},
},
Spec: federationapi.ClusterSpec{
ServerAddressByClientCIDRs: []federationapi.ServerAddressByClientCIDR{
{
ClientCIDR: "10.20.30.40/16",
ServerAddress: "http://10.20.30.40",
},
},
SecretRef: &v1.LocalObjectReference{
Name: "credentials",
},
},
},
expectErr: false,
},
{
params: map[string]interface{}{
"server-address": "https://10.20.30.40",
},
expected: nil,
......@@ -144,6 +200,28 @@ func TestClusterGenerate(t *testing.T) {
expected: nil,
expectErr: true,
},
{
params: map[string]interface{}{
"name": "bar-cluster",
"client-cidr": "10.20.30.40/16",
"server-address": "http://10.20.30.40",
"secret": "credentials",
"cluster-role-name": "cluster-role",
},
expected: nil,
expectErr: true,
},
{
params: map[string]interface{}{
"name": "bar-cluster",
"client-cidr": "10.20.30.40/16",
"server-address": "http://10.20.30.40",
"secret": "credentials",
"service-account-name": "service-account",
},
expected: nil,
expectErr: true,
},
}
generator := ClusterGeneratorV1Beta1{}
for i, test := range tests {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment