...

Source file src/github.com/linkerd/linkerd2/cli/cmd/install.go

Documentation: github.com/linkerd/linkerd2/cli/cmd

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"strconv"
    11  	"strings"
    12  	"text/template"
    13  	"time"
    14  
    15  	"github.com/linkerd/linkerd2/cli/flag"
    16  	"github.com/linkerd/linkerd2/pkg/charts"
    17  	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
    18  	"github.com/linkerd/linkerd2/pkg/charts/static"
    19  	flagspkg "github.com/linkerd/linkerd2/pkg/flags"
    20  	"github.com/linkerd/linkerd2/pkg/healthcheck"
    21  	"github.com/linkerd/linkerd2/pkg/k8s"
    22  	"github.com/linkerd/linkerd2/pkg/tree"
    23  	"github.com/spf13/cobra"
    24  	"helm.sh/helm/v3/pkg/chart/loader"
    25  	"helm.sh/helm/v3/pkg/chartutil"
    26  	valuespkg "helm.sh/helm/v3/pkg/cli/values"
    27  	"helm.sh/helm/v3/pkg/engine"
    28  	corev1 "k8s.io/api/core/v1"
    29  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/util/intstr"
    32  	"sigs.k8s.io/yaml"
    33  )
    34  
    35  const (
    36  	helmDefaultChartNameCrds = "linkerd-crds"
    37  	helmDefaultChartNameCP   = "linkerd-control-plane"
    38  
    39  	errMsgCannotInitializeClient = `Unable to install the Linkerd control plane. Cannot connect to the Kubernetes cluster:
    40  
    41  %s
    42  
    43  You can use the --ignore-cluster flag if you just want to generate the installation config.
    44  `
    45  
    46  	errMsgLinkerdConfigResourceConflict = "Can't install the Linkerd control plane in the '%s' namespace. Reason: %s.\nRun the command `linkerd upgrade`, if you are looking to upgrade Linkerd.\n"
    47  )
    48  
    49  var (
    50  	TemplatesCrdFiles = []string{
    51  		"templates/policy/authorization-policy.yaml",
    52  		"templates/policy/httproute.yaml",
    53  		"templates/policy/meshtls-authentication.yaml",
    54  		"templates/policy/network-authentication.yaml",
    55  		"templates/policy/server-authorization.yaml",
    56  		"templates/policy/server.yaml",
    57  		"templates/serviceprofile.yaml",
    58  		"templates/gateway.networking.k8s.io_httproutes.yaml",
    59  		"templates/gateway.networking.k8s.io_grpcroutes.yaml",
    60  		"templates/workload/external-workload.yaml",
    61  	}
    62  
    63  	TemplatesControlPlane = []string{
    64  		"templates/namespace.yaml",
    65  		"templates/identity-rbac.yaml",
    66  		"templates/destination-rbac.yaml",
    67  		"templates/heartbeat-rbac.yaml",
    68  		"templates/podmonitor.yaml",
    69  		"templates/proxy-injector-rbac.yaml",
    70  		"templates/psp.yaml",
    71  		"templates/config.yaml",
    72  		"templates/config-rbac.yaml",
    73  		"templates/identity.yaml",
    74  		"templates/destination.yaml",
    75  		"templates/heartbeat.yaml",
    76  		"templates/proxy-injector.yaml",
    77  	}
    78  
    79  	ignoreCluster bool
    80  )
    81  
    82  /* Commands */
    83  func newCmdInstall() *cobra.Command {
    84  	values, err := l5dcharts.NewValues()
    85  	if err != nil {
    86  		fmt.Fprintln(os.Stderr, err.Error())
    87  		os.Exit(1)
    88  	}
    89  	var crds bool
    90  	var options valuespkg.Options
    91  
    92  	installOnlyFlags, installOnlyFlagSet := makeInstallFlags(values)
    93  	installUpgradeFlags, installUpgradeFlagSet, err := makeInstallUpgradeFlags(values)
    94  	if err != nil {
    95  		fmt.Fprint(os.Stderr, err.Error())
    96  		os.Exit(1)
    97  	}
    98  	proxyFlags, proxyFlagSet := makeProxyFlags(values)
    99  
   100  	flags := flattenFlags(installOnlyFlags, installUpgradeFlags, proxyFlags)
   101  
   102  	cmd := &cobra.Command{
   103  		Use:   "install [flags]",
   104  		Args:  cobra.NoArgs,
   105  		Short: "Output Kubernetes configs to install Linkerd",
   106  		Long: `Output Kubernetes configs to install Linkerd.
   107  
   108  This command provides all Kubernetes configs necessary to install the Linkerd
   109  control plane.`,
   110  		Example: `  # Install CRDs first.
   111    linkerd install --crds | kubectl apply -f -
   112  
   113    # Install the core control plane.
   114    linkerd install | kubectl apply -f -
   115  
   116  The installation can be configured by using the --set, --values, --set-string and --set-file flags.
   117  A full list of configurable values can be found at https://artifacthub.io/packages/helm/linkerd2/linkerd-control-plane#values`,
   118  		RunE: func(cmd *cobra.Command, args []string) error {
   119  			var k8sAPI *k8s.KubernetesAPI
   120  			if !ignoreCluster {
   121  				// Ensure k8s is reachable
   122  				if err := errAfterRunningChecks(values.CNIEnabled); err != nil {
   123  					fmt.Fprintf(os.Stderr, errMsgCannotInitializeClient, err)
   124  					os.Exit(1)
   125  				}
   126  
   127  				// Ensure there is not already an existing Linkerd installation.
   128  				k8sAPI, err = k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 30*time.Second)
   129  				if err != nil {
   130  					return err
   131  				}
   132  
   133  				if !crds {
   134  					crds := bytes.Buffer{}
   135  					err := renderCRDs(&crds, options)
   136  					if err != nil {
   137  						fmt.Fprintf(os.Stderr, "%q", err)
   138  						os.Exit(1)
   139  					}
   140  					err = healthcheck.CheckCustomResourceDefinitions(cmd.Context(), k8sAPI, crds.String())
   141  					if err != nil {
   142  						fmt.Fprintln(os.Stderr, "Linkerd CRDs must be installed first. Run linkerd install with the --crds flag.")
   143  						os.Exit(1)
   144  					}
   145  				}
   146  			}
   147  
   148  			if crds {
   149  				// The CRD chart is not configurable.
   150  				// TODO(ver): Error if values have been configured?
   151  				if err = installCRDs(cmd.Context(), k8sAPI, os.Stdout, options); err != nil {
   152  					return err
   153  				}
   154  
   155  				fmt.Fprintln(os.Stderr, "Rendering Linkerd CRDs...")
   156  				fmt.Fprintln(os.Stderr, "Next, run `linkerd install | kubectl apply -f -` to install the control plane.")
   157  				fmt.Fprintln(os.Stderr)
   158  				return nil
   159  			}
   160  
   161  			return installControlPlane(cmd.Context(), k8sAPI, os.Stdout, values, flags, options)
   162  		},
   163  	}
   164  
   165  	cmd.Flags().AddFlagSet(installOnlyFlagSet)
   166  	cmd.Flags().AddFlagSet(installUpgradeFlagSet)
   167  	cmd.Flags().AddFlagSet(proxyFlagSet)
   168  	cmd.Flags().BoolVar(&crds, "crds", false, "Install Linkerd CRDs")
   169  	cmd.PersistentFlags().BoolVar(&ignoreCluster, "ignore-cluster", false,
   170  		"Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)")
   171  
   172  	flagspkg.AddValueOptionsFlags(cmd.Flags(), &options)
   173  
   174  	return cmd
   175  }
   176  
   177  func checkNoConfig(ctx context.Context, k8sAPI *k8s.KubernetesAPI) error {
   178  	if k8sAPI == nil {
   179  		// When `ingoreCluster` is set, there is no k8sAPI.
   180  		return nil
   181  	}
   182  
   183  	// We just want to check if `linkerd-configmap` exists
   184  	_, err := k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{})
   185  	if err == nil {
   186  		fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, "ConfigMap/linkerd-config already exists")
   187  		os.Exit(1)
   188  	}
   189  	if !kerrors.IsNotFound(err) {
   190  		return err
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  func installCRDs(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Writer, options valuespkg.Options) error {
   197  	if err := checkNoConfig(ctx, k8sAPI); err != nil {
   198  		return err
   199  	}
   200  
   201  	return renderCRDs(w, options)
   202  }
   203  
   204  func installControlPlane(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Writer, values *l5dcharts.Values, flags []flag.Flag, options valuespkg.Options) error {
   205  	err := flag.ApplySetFlags(values, flags)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	if err := checkNoConfig(ctx, k8sAPI); err != nil {
   211  		return err
   212  	}
   213  
   214  	// Create values override
   215  	valuesOverrides, err := options.MergeValues(nil)
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	if k8sAPI != nil {
   221  		// We just want to check if `linkerd-configmap` exists
   222  		_, err = k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{})
   223  		if err == nil {
   224  			fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, "ConfigMap/linkerd-config already exists")
   225  			os.Exit(1)
   226  		}
   227  		if !kerrors.IsNotFound(err) {
   228  			return err
   229  		}
   230  
   231  		if !isRunAsRoot(valuesOverrides) {
   232  			err = healthcheck.CheckNodesHaveNonDockerRuntime(ctx, k8sAPI)
   233  			if err != nil {
   234  				fmt.Fprintln(os.Stderr, err)
   235  				os.Exit(1)
   236  			}
   237  		}
   238  
   239  		// Check 'kubernetes' service in default namespace to see what ports the API
   240  		// Server listens on. If the ports are different from the default ('443,6443')
   241  		// then replace with ports from the service spec.
   242  		apiSrvPorts := getApiServerPorts(ctx, k8sAPI)
   243  		if apiSrvPorts != "" {
   244  			values.ProxyInit.KubeAPIServerPorts = apiSrvPorts
   245  		}
   246  	}
   247  
   248  	err = initializeIssuerCredentials(ctx, k8sAPI, values)
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	err = validateValues(ctx, k8sAPI, values)
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	return renderControlPlane(w, values, valuesOverrides)
   259  }
   260  
   261  func isRunAsRoot(values map[string]interface{}) bool {
   262  	if proxyInit, ok := values["proxyInit"]; ok {
   263  		if val, ok := proxyInit.(map[string]interface{})["runAsRoot"]; ok {
   264  			if truth, ok := template.IsTrue(val); ok {
   265  				return truth
   266  			}
   267  		}
   268  	}
   269  	return false
   270  }
   271  
   272  // renderChartToBuffer takes a slice of loaded template files and configuration values and renders
   273  // them into a buffer. The coalesced values are also returned so that they may be rendered via
   274  // `renderOverrides` if appropriate.
   275  func renderChartToBuffer(files []*loader.BufferedFile, values map[string]interface{}, valuesOverrides map[string]interface{}) (*bytes.Buffer, chartutil.Values, error) {
   276  	// Load the partials in addition to the main chart.
   277  	var partials []*loader.BufferedFile
   278  	for _, template := range charts.L5dPartials {
   279  		partials = append(partials, &loader.BufferedFile{Name: template})
   280  	}
   281  	if err := charts.FilesReader(static.Templates, "", partials); err != nil {
   282  		return nil, nil, err
   283  	}
   284  	chart, err := loader.LoadFiles(append(files, partials...))
   285  	if err != nil {
   286  		return nil, nil, err
   287  	}
   288  
   289  	// Store final Values generated from values.yaml and CLI flags
   290  	chart.Values = values
   291  
   292  	vals, err := chartutil.CoalesceValues(chart, valuesOverrides)
   293  	if err != nil {
   294  		return nil, nil, err
   295  	}
   296  
   297  	fullValues := map[string]interface{}{
   298  		"Values": vals,
   299  		"Release": map[string]interface{}{
   300  			"Namespace": controlPlaneNamespace,
   301  			"Service":   "CLI",
   302  		},
   303  	}
   304  
   305  	// Attach the final values into the `Values` field for rendering to work
   306  	renderedTemplates, err := engine.Render(chart, fullValues)
   307  	if err != nil {
   308  		return nil, nil, fmt.Errorf("failed to render the template: %w", err)
   309  	}
   310  
   311  	// Merge templates and inject
   312  	var buf bytes.Buffer
   313  	for _, tmpl := range chart.Templates {
   314  		t := path.Join(chart.Metadata.Name, tmpl.Name)
   315  		if _, err := buf.WriteString(renderedTemplates[t]); err != nil {
   316  			return nil, nil, err
   317  		}
   318  	}
   319  
   320  	return &buf, vals, nil
   321  }
   322  
   323  func renderCRDs(w io.Writer, options valuespkg.Options) error {
   324  	files := []*loader.BufferedFile{
   325  		{Name: chartutil.ChartfileName},
   326  	}
   327  	for _, template := range TemplatesCrdFiles {
   328  		files = append(files, &loader.BufferedFile{Name: template})
   329  	}
   330  	if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCrds+"/", files); err != nil {
   331  		return err
   332  	}
   333  
   334  	// Load defaults from values.yaml
   335  	valuesFile := &loader.BufferedFile{Name: l5dcharts.HelmChartDirCrds + "/values.yaml"}
   336  	if err := charts.ReadFile(static.Templates, "/", valuesFile); err != nil {
   337  		return err
   338  	}
   339  	// Ensure the map is not nil, even if the default `values.yaml` is empty ---
   340  	// if there are no values in the YAML file, `yaml.Unmarshal` will not
   341  	// allocate the map, and the subsequent assignment to `cliVersion` will
   342  	// panic because the map is nil.
   343  	defaultValues := make(map[string]interface{})
   344  	err := yaml.Unmarshal(valuesFile.Data, &defaultValues)
   345  	if err != nil {
   346  		return err
   347  	}
   348  	defaultValues["cliVersion"] = k8s.CreatedByAnnotationValue()
   349  
   350  	// Create values override
   351  	valuesOverrides, err := options.MergeValues(nil)
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	buf, _, err := renderChartToBuffer(files, defaultValues, valuesOverrides)
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	_, err = w.Write(buf.Bytes())
   362  	return err
   363  }
   364  
   365  func renderControlPlane(w io.Writer, values *l5dcharts.Values, valuesOverrides map[string]interface{}) error {
   366  	files := []*loader.BufferedFile{
   367  		{Name: chartutil.ChartfileName},
   368  	}
   369  	for _, template := range TemplatesControlPlane {
   370  		files = append(files, &loader.BufferedFile{Name: template})
   371  	}
   372  	if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCP+"/", files); err != nil {
   373  		return err
   374  	}
   375  
   376  	valuesMap, err := values.ToMap()
   377  	if err != nil {
   378  		return err
   379  	}
   380  	buf, vals, err := renderChartToBuffer(files, valuesMap, valuesOverrides)
   381  	if err != nil {
   382  		return err
   383  	}
   384  
   385  	overrides, err := renderOverrides(vals, false)
   386  	if err != nil {
   387  		return err
   388  	}
   389  	buf.WriteString(yamlSep)
   390  	buf.WriteString(string(overrides))
   391  
   392  	_, err = w.Write(buf.Bytes())
   393  	return err
   394  }
   395  
   396  // renderOverrides outputs the Secret/linkerd-config-overrides resource which
   397  // contains the subset of the values which have been changed from their defaults.
   398  // This secret is used by the upgrade command the load configuration which was
   399  // specified at install time.  Note that if identity issuer credentials were
   400  // supplied to the install command or if they were generated by the install
   401  // command, those credentials will be saved here so that they are preserved
   402  // during upgrade.  Note also that this Secret/linkerd-config-overrides
   403  // resource is not part of the Helm chart and will not be present when installing
   404  // with Helm. If stringData is set to true, the secret will be rendered using
   405  // the StringData field instead of the Data field, making the output more
   406  // human readable.
   407  func renderOverrides(values chartutil.Values, stringData bool) ([]byte, error) {
   408  	defaults, err := l5dcharts.NewValues()
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  	// Remove unnecessary fields, including fields added by helm's `chartutil.CoalesceValues`
   413  	delete(values, "configs")
   414  	delete(values, "partials")
   415  
   416  	overrides, err := tree.Diff(defaults, values)
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  
   421  	overridesBytes, err := yaml.Marshal(overrides)
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  
   426  	secret := corev1.Secret{
   427  		TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
   428  		ObjectMeta: metav1.ObjectMeta{
   429  			Name:      "linkerd-config-overrides",
   430  			Namespace: controlPlaneNamespace,
   431  			Labels: map[string]string{
   432  				k8s.ControllerNSLabel: controlPlaneNamespace,
   433  			},
   434  		},
   435  	}
   436  	if stringData {
   437  		secret.StringData = map[string]string{
   438  			"linkerd-config-overrides": string(overridesBytes),
   439  		}
   440  	} else {
   441  		secret.Data = map[string][]byte{
   442  			"linkerd-config-overrides": overridesBytes,
   443  		}
   444  	}
   445  	bytes, err := yaml.Marshal(secret)
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  	return bytes, nil
   450  }
   451  
   452  func errAfterRunningChecks(cniEnabled bool) error {
   453  	checks := []healthcheck.CategoryID{
   454  		healthcheck.KubernetesAPIChecks,
   455  	}
   456  	hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
   457  		ControlPlaneNamespace: controlPlaneNamespace,
   458  		KubeConfig:            kubeconfigPath,
   459  		Impersonate:           impersonate,
   460  		ImpersonateGroup:      impersonateGroup,
   461  		KubeContext:           kubeContext,
   462  		APIAddr:               apiAddr,
   463  		CNIEnabled:            cniEnabled,
   464  	})
   465  
   466  	var err error
   467  	hc.RunChecks(func(result *healthcheck.CheckResult) {
   468  		if result.Err != nil {
   469  			err = result.Err
   470  		}
   471  	})
   472  
   473  	return err
   474  }
   475  
   476  // getApiServerPorts looks at the 'kubernetes' service in the 'default'
   477  // namespace and returns the ClusterIP port for the API Server (by default 443),
   478  // and the port that the API Server backend is expecting TLS connections on (by
   479  // default 6443.)
   480  func getApiServerPorts(ctx context.Context, api *k8s.KubernetesAPI) string {
   481  	service, err := api.CoreV1().Services("default").Get(ctx, "kubernetes", metav1.GetOptions{})
   482  	if err != nil {
   483  		return ""
   484  	}
   485  
   486  	ports := make([]string, 0)
   487  	for _, port := range service.Spec.Ports {
   488  		ports = append(ports, strconv.Itoa(int(port.Port)))
   489  		// We only care about int ports since string ports (e.g targetPort: web)
   490  		// correspond to a named port in a pod spec.
   491  		if port.TargetPort.Type == intstr.Int {
   492  			ports = append(ports, strconv.Itoa(port.TargetPort.IntValue()))
   493  		}
   494  	}
   495  
   496  	return strings.Join(ports, ",")
   497  }
   498  

View as plain text