mirror of https://github.com/linkerd/linkerd2.git
378 lines
12 KiB
Go
378 lines
12 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/linkerd/linkerd2/multicluster/static"
|
|
multicluster "github.com/linkerd/linkerd2/multicluster/values"
|
|
"github.com/linkerd/linkerd2/pkg/charts"
|
|
"github.com/linkerd/linkerd2/pkg/flags"
|
|
"github.com/linkerd/linkerd2/pkg/k8s"
|
|
mc "github.com/linkerd/linkerd2/pkg/multicluster"
|
|
"github.com/linkerd/linkerd2/pkg/version"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
chartloader "helm.sh/helm/v3/pkg/chart/loader"
|
|
"helm.sh/helm/v3/pkg/chartutil"
|
|
valuespkg "helm.sh/helm/v3/pkg/cli/values"
|
|
"helm.sh/helm/v3/pkg/engine"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
"k8s.io/client-go/tools/clientcmd/api"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
type (
|
|
linkOptions struct {
|
|
namespace string
|
|
clusterName string
|
|
apiServerAddress string
|
|
serviceAccountName string
|
|
gatewayName string
|
|
gatewayNamespace string
|
|
serviceMirrorRetryLimit uint32
|
|
logLevel string
|
|
controlPlaneVersion string
|
|
dockerRegistry string
|
|
selector string
|
|
gatewayAddresses string
|
|
}
|
|
)
|
|
|
|
func newLinkCommand() *cobra.Command {
|
|
opts, err := newLinkOptionsWithDefault()
|
|
var valuesOptions valuespkg.Options
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "link",
|
|
Short: "Outputs resources that allow another cluster to mirror services from this one",
|
|
Args: cobra.NoArgs,
|
|
Example: ` # To link the west cluster to east
|
|
linkerd --context=east multicluster link --cluster-name east | kubectl --context=west apply -f -
|
|
|
|
The command can be configured by using the --set, --values, --set-string and --set-file flags.
|
|
A full list of configurable values can be found at https://github.com/linkerd/linkerd2/blob/main/multicluster/charts/linkerd2-multicluster-link/README.md
|
|
`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
if opts.clusterName == "" {
|
|
return errors.New("You need to specify cluster name")
|
|
}
|
|
|
|
configMap, err := getLinkerdConfigMap(cmd.Context())
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
return errors.New("you need Linkerd to be installed on a cluster in order to get its credentials")
|
|
}
|
|
return err
|
|
}
|
|
|
|
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(opts.namespace).Get(cmd.Context(), opts.serviceAccountName, 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(opts.namespace).Get(cmd.Context(), 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 = opts.serviceAccountName
|
|
config.Contexts = map[string]*api.Context{
|
|
config.CurrentContext: context,
|
|
}
|
|
config.AuthInfos = map[string]*api.AuthInfo{
|
|
opts.serviceAccountName: {
|
|
Token: string(token),
|
|
},
|
|
}
|
|
|
|
cluster := config.Clusters[context.Cluster]
|
|
|
|
if opts.apiServerAddress != "" {
|
|
cluster.Server = opts.apiServerAddress
|
|
}
|
|
|
|
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", opts.clusterName),
|
|
Namespace: opts.namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
k8s.ConfigKeyName: kubeconfig,
|
|
},
|
|
}
|
|
|
|
credsOut, err := yaml.Marshal(creds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gateway, err := k.CoreV1().Services(opts.gatewayNamespace).Get(cmd.Context(), opts.gatewayName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gatewayAddresses := ""
|
|
gwAddresses := []string{}
|
|
for _, ingress := range gateway.Status.LoadBalancer.Ingress {
|
|
addr := ingress.IP
|
|
if addr == "" {
|
|
addr = ingress.Hostname
|
|
}
|
|
if addr == "" {
|
|
continue
|
|
}
|
|
gwAddresses = append(gwAddresses, addr)
|
|
}
|
|
if len(gwAddresses) == 0 && opts.gatewayAddresses == "" {
|
|
return fmt.Errorf("Gateway %s.%s has no ingress addresses", gateway.Name, gateway.Namespace)
|
|
} else if len(gwAddresses) > 0 {
|
|
gatewayAddresses = strings.Join(gwAddresses, ",")
|
|
} else {
|
|
gatewayAddresses = opts.gatewayAddresses
|
|
}
|
|
|
|
gatewayIdentity, ok := gateway.Annotations[k8s.GatewayIdentity]
|
|
if !ok || gatewayIdentity == "" {
|
|
return fmt.Errorf("Gateway %s.%s has no %s annotation", gateway.Name, gateway.Namespace, k8s.GatewayIdentity)
|
|
}
|
|
|
|
probeSpec, err := mc.ExtractProbeSpec(gateway)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gatewayPort, err := extractGatewayPort(gateway)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
selector, err := metav1.ParseToLabelSelector(opts.selector)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
link := mc.Link{
|
|
Name: opts.clusterName,
|
|
Namespace: opts.namespace,
|
|
TargetClusterName: opts.clusterName,
|
|
TargetClusterDomain: configMap.GetGlobal().ClusterDomain,
|
|
TargetClusterLinkerdNamespace: controlPlaneNamespace,
|
|
ClusterCredentialsSecret: fmt.Sprintf("cluster-credentials-%s", opts.clusterName),
|
|
GatewayAddress: gatewayAddresses,
|
|
GatewayPort: gatewayPort,
|
|
GatewayIdentity: gatewayIdentity,
|
|
ProbeSpec: probeSpec,
|
|
Selector: *selector,
|
|
}
|
|
|
|
obj, err := link.ToUnstructured()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
linkOut, err := yaml.Marshal(obj.Object)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
values, err := buildServiceMirrorValues(opts)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Render raw values and create chart config
|
|
rawValues, err := yaml.Marshal(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
files := []*chartloader.BufferedFile{
|
|
{Name: chartutil.ChartfileName},
|
|
{Name: "templates/service-mirror.yaml"},
|
|
{Name: "templates/gateway-mirror.yaml"},
|
|
}
|
|
|
|
// Load all multicluster link chart files into buffer
|
|
if err := charts.FilesReader(static.Templates, helmMulticlusterLinkDefaultChartName+"/", files); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a Chart obj from the files
|
|
chart, err := chartloader.LoadFiles(files)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store final Values generated from values.yaml and CLI flags
|
|
err = yaml.Unmarshal(rawValues, &chart.Values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create values override
|
|
valuesOverrides, err := valuesOptions.MergeValues(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
vals, err := chartutil.CoalesceValues(chart, valuesOverrides)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Attach the final values into the `Values` field for rendering to work
|
|
renderedTemplates, err := engine.Render(chart, map[string]interface{}{"Values": vals})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Merge templates and inject
|
|
var serviceMirrorOut bytes.Buffer
|
|
for _, tmpl := range chart.Templates {
|
|
t := path.Join(chart.Metadata.Name, tmpl.Name)
|
|
if _, err := serviceMirrorOut.WriteString(renderedTemplates[t]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
stdout.Write(credsOut)
|
|
stdout.Write([]byte("---\n"))
|
|
stdout.Write(linkOut)
|
|
stdout.Write([]byte("---\n"))
|
|
stdout.Write(serviceMirrorOut.Bytes())
|
|
stdout.Write([]byte("---\n"))
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
flags.AddValueOptionsFlags(cmd.Flags(), &valuesOptions)
|
|
cmd.Flags().StringVar(&opts.namespace, "namespace", defaultMulticlusterNamespace, "The namespace for the service account")
|
|
cmd.Flags().StringVar(&opts.clusterName, "cluster-name", "", "Cluster name")
|
|
cmd.Flags().StringVar(&opts.apiServerAddress, "api-server-address", "", "The api server address of the target cluster")
|
|
cmd.Flags().StringVar(&opts.serviceAccountName, "service-account-name", defaultServiceAccountName, "The name of the service account associated with the credentials")
|
|
cmd.Flags().StringVar(&opts.controlPlaneVersion, "control-plane-version", opts.controlPlaneVersion, "(Development) Tag to be used for the service mirror controller image")
|
|
cmd.Flags().StringVar(&opts.gatewayName, "gateway-name", defaultGatewayName, "The name of the gateway service")
|
|
cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", defaultMulticlusterNamespace, "The namespace of the gateway service")
|
|
cmd.Flags().Uint32Var(&opts.serviceMirrorRetryLimit, "service-mirror-retry-limit", opts.serviceMirrorRetryLimit, "The number of times a failed update from the target cluster is allowed to be retried")
|
|
cmd.Flags().StringVar(&opts.logLevel, "log-level", opts.logLevel, "Log level for the Multicluster components")
|
|
cmd.Flags().StringVar(&opts.dockerRegistry, "registry", opts.dockerRegistry, "Docker registry to pull service mirror controller image from")
|
|
cmd.Flags().StringVarP(&opts.selector, "selector", "l", opts.selector, "Selector (label query) to filter which services in the target cluster to mirror")
|
|
cmd.Flags().StringVar(&opts.gatewayAddresses, "gateway-addresses", opts.gatewayAddresses, "If specified overwrites gateway addresses when gateway service is not type LoadBalancer (comma separated list)")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newLinkOptionsWithDefault() (*linkOptions, error) {
|
|
defaults, err := multicluster.NewLinkValues()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &linkOptions{
|
|
controlPlaneVersion: version.Version,
|
|
namespace: defaults.Namespace,
|
|
dockerRegistry: defaultDockerRegistry,
|
|
serviceMirrorRetryLimit: defaults.ServiceMirrorRetryLimit,
|
|
logLevel: defaults.LogLevel,
|
|
selector: k8s.DefaultExportedServiceSelector,
|
|
gatewayAddresses: "",
|
|
}, nil
|
|
}
|
|
|
|
func buildServiceMirrorValues(opts *linkOptions) (*multicluster.Values, error) {
|
|
|
|
if !alphaNumDashDot.MatchString(opts.controlPlaneVersion) {
|
|
return nil, fmt.Errorf("%s is not a valid version", opts.controlPlaneVersion)
|
|
}
|
|
|
|
if opts.namespace == "" {
|
|
return nil, errors.New("you need to specify a namespace")
|
|
}
|
|
|
|
if opts.namespace == controlPlaneNamespace {
|
|
return nil, errors.New("you need to setup the multicluster addons in a namespace different than the Linkerd one")
|
|
}
|
|
|
|
if _, err := log.ParseLevel(opts.logLevel); err != nil {
|
|
return nil, fmt.Errorf("--log-level must be one of: panic, fatal, error, warn, info, debug")
|
|
}
|
|
|
|
defaults, err := multicluster.NewLinkValues()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defaults.TargetClusterName = opts.clusterName
|
|
defaults.Namespace = opts.namespace
|
|
defaults.ServiceMirrorRetryLimit = opts.serviceMirrorRetryLimit
|
|
defaults.LogLevel = opts.logLevel
|
|
defaults.ControllerImageVersion = opts.controlPlaneVersion
|
|
defaults.ControllerImage = fmt.Sprintf("%s/controller", opts.dockerRegistry)
|
|
|
|
return defaults, nil
|
|
}
|