linkerd2/cli/cmd/cluster.go

299 lines
9.5 KiB
Go

package cmd
import (
"fmt"
"strings"
v1 "k8s.io/api/rbac/v1"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
const (
tokenKey = "token"
defaultServiceAccountName = "linkerd-service-mirror"
defaultServiceAccountNs = "default"
defaultClusterName = "remote"
)
type (
getCredentialsOptions struct {
namespace string
serviceAccount string
clusterName string
remoteClusterDomain string
}
createOptions struct {
namespace string
serviceAccount string
}
exportServiceOptions struct {
namespace string
service string
gatewayNamespace string
gatewayName string
}
)
func newCmdCluster() *cobra.Command {
getOpts := getCredentialsOptions{}
createOpts := createOptions{}
exportOpts := exportServiceOptions{}
clusterCmd := &cobra.Command{
Hidden: true,
Use: "cluster [flags]",
Args: cobra.NoArgs,
Short: "Manages the multicluster setup for Linkerd",
Long: `Manages the multicluster setup for Linkerd.
This command provides subcommands to manage the multicluster support
functionality of Linkerd. You can use it to deploy credentials to
remote clusters, extract them as well as export remote services to be
available across clusters.`,
Example: ` # Create remote cluster credentials.
linkerd --context=cluster-a cluster create-credentials | kubectl --context=cluster-a apply -f -
# Extract mirroring cluster credentials from cluster A and install them on cluster B
linkerd --context=cluster-a cluster get-credentials --cluster-name=remote | kubectl apply --context=cluster-b -f -
# Export service from cluster A to be available to other clusters
linkerd --context=cluster-a cluster export-service --service-name=backend-svc --service-namespace=default --gateway-name=linkerd-gateway --gateway-ns=default`,
}
createCredentialsCommand := &cobra.Command{
Hidden: false,
Use: "create-credentials",
Short: "Create the necessary credentials for service mirroring",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
labels := map[string]string{
k8s.ControllerComponentLabel: k8s.ServiceMirrorLabel,
k8s.ControllerNSLabel: controlPlaneNamespace,
}
clusterRole := v1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
TypeMeta: metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"services"},
Verbs: []string{"list", "get", "watch"},
},
},
}
svcAccount := corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
TypeMeta: metav1.TypeMeta{Kind: v1.ServiceAccountKind, APIVersion: "v1"},
}
clusterRoleBinding := v1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
Subjects: []v1.Subject{
v1.Subject{Kind: v1.ServiceAccountKind, Name: createOpts.serviceAccount, Namespace: createOpts.namespace},
},
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: createOpts.serviceAccount},
}
crOut, err := yaml.Marshal(clusterRole)
if err != nil {
return err
}
saOut, err := yaml.Marshal(svcAccount)
if err != nil {
return err
}
crbOut, err := yaml.Marshal(clusterRoleBinding)
if err != nil {
return err
}
fmt.Println(fmt.Sprintf("---\n%s---\n%s---\n%s", crOut, saOut, crbOut))
return nil
},
}
getCredentialsCmd := &cobra.Command{
Hidden: false,
Use: "get-credentials",
Short: "Get cluster credentials as a secret",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = kubeconfigPath
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
config, err := loader.RawConfig()
if err != nil {
return err
}
if kubeContext != "" {
config.CurrentContext = kubeContext
}
k, err := k8s.NewAPI(kubeconfigPath, config.CurrentContext, impersonate, impersonateGroup, 0)
if err != nil {
return err
}
sa, err := k.CoreV1().ServiceAccounts(getOpts.namespace).Get(getOpts.serviceAccount, metav1.GetOptions{})
if err != nil {
return err
}
var secretName string
for _, s := range sa.Secrets {
if strings.HasPrefix(s.Name, fmt.Sprintf("%s-token", sa.Name)) {
secretName = s.Name
break
}
}
if secretName == "" {
return fmt.Errorf("could not find service account token secret for %s", sa.Name)
}
secret, err := k.CoreV1().Secrets(getOpts.namespace).Get(secretName, metav1.GetOptions{})
if err != nil {
return err
}
token, ok := secret.Data[tokenKey]
if !ok {
return fmt.Errorf("could not find the token data in the service account secret")
}
context, ok := config.Contexts[config.CurrentContext]
if !ok {
return fmt.Errorf("could not extract current context from config")
}
context.AuthInfo = getOpts.serviceAccount
config.Contexts = map[string]*api.Context{
config.CurrentContext: context,
}
config.AuthInfos = map[string]*api.AuthInfo{
getOpts.serviceAccount: {
Token: string(token),
},
}
cluster := config.Clusters[context.Cluster]
config.Clusters = map[string]*api.Cluster{
context.Cluster: cluster,
}
kubeconfig, err := clientcmd.Write(config)
if err != nil {
return err
}
creds := corev1.Secret{
Type: k8s.MirrorSecretType,
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("cluster-credentials-%s", getOpts.clusterName),
Annotations: map[string]string{
k8s.RemoteClusterNameLabel: getOpts.clusterName,
k8s.RemoteClusterDomainAnnotation: getOpts.remoteClusterDomain,
},
},
Data: map[string][]byte{
k8s.ConfigKeyName: kubeconfig,
},
}
out, err := yaml.Marshal(creds)
if err != nil {
return err
}
fmt.Println(string(out))
return nil
},
}
exportServiceCmd := &cobra.Command{
Hidden: false,
Use: "export-service",
Short: "Exposes a remote service to be mirrored",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = kubeconfigPath
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
config, err := loader.RawConfig()
if err != nil {
return err
}
if kubeContext != "" {
config.CurrentContext = kubeContext
}
k, err := k8s.NewAPI(kubeconfigPath, config.CurrentContext, impersonate, impersonateGroup, 0)
if err != nil {
return err
}
svc, err := k.CoreV1().Services(exportOpts.namespace).Get(exportOpts.service, metav1.GetOptions{})
if err != nil {
return err
}
_, hasGatewayName := svc.Annotations[k8s.GatewayNameAnnotation]
_, hasGatewayNs := svc.Annotations[k8s.GatewayNsAnnotation]
if hasGatewayName || hasGatewayNs {
return fmt.Errorf("service %s/%s has already been exported", svc.Namespace, svc.Name)
}
svc.Annotations[k8s.GatewayNameAnnotation] = exportOpts.gatewayName
svc.Annotations[k8s.GatewayNsAnnotation] = exportOpts.gatewayNamespace
_, err = k.CoreV1().Services(svc.Namespace).Update(svc)
if err != nil {
return err
}
fmt.Println(fmt.Sprintf("Service %s/%s is now exported", svc.Namespace, svc.Name))
return nil
},
}
getCredentialsCmd.Flags().StringVar(&getOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account")
getCredentialsCmd.Flags().StringVar(&getOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account will be created")
getCredentialsCmd.Flags().StringVar(&getOpts.clusterName, "cluster-name", defaultClusterName, "cluster name")
getCredentialsCmd.Flags().StringVar(&getOpts.remoteClusterDomain, "remote-cluster-domain", defaultClusterDomain, "custom remote cluster domain")
createCredentialsCommand.Flags().StringVar(&createOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account used")
createCredentialsCommand.Flags().StringVar(&createOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account can be found")
exportServiceCmd.Flags().StringVar(&exportOpts.service, "service-name", "", "the name of the service to be exported")
exportServiceCmd.Flags().StringVar(&exportOpts.namespace, "service-namespace", "", "the namespace in which the service to be exported resides")
exportServiceCmd.Flags().StringVar(&exportOpts.gatewayName, "gateway-name", "", "the name of the gateway")
exportServiceCmd.Flags().StringVar(&exportOpts.gatewayNamespace, "gateway-ns", "", "the ns of the gateway")
clusterCmd.AddCommand(getCredentialsCmd)
clusterCmd.AddCommand(createCredentialsCommand)
clusterCmd.AddCommand(exportServiceCmd)
return clusterCmd
}