linkerd2/cli/cmd/install.go

454 lines
13 KiB
Go

package cmd
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path"
"text/template"
"time"
"github.com/linkerd/linkerd2/cli/flag"
"github.com/linkerd/linkerd2/pkg/charts"
l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
"github.com/linkerd/linkerd2/pkg/charts/static"
flagspkg "github.com/linkerd/linkerd2/pkg/flags"
"github.com/linkerd/linkerd2/pkg/healthcheck"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/tree"
"github.com/spf13/cobra"
"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"
"sigs.k8s.io/yaml"
)
const (
helmDefaultChartNameCrds = "linkerd-crds"
helmDefaultChartNameCP = "linkerd-control-plane"
errMsgCannotInitializeClient = `Unable to install the Linkerd control plane. Cannot connect to the Kubernetes cluster:
%s
You can use the --ignore-cluster flag if you just want to generate the installation config.`
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"
)
var (
templatesCrdFiles = []string{
"templates/gateway.networking.k8s.io/httproute.yaml",
"templates/policy/authorization-policy.yaml",
"templates/policy/meshtls-authentication.yaml",
"templates/policy/network-authentication.yaml",
"templates/policy/server-authorization.yaml",
"templates/policy/server.yaml",
"templates/serviceprofile.yaml",
}
templatesControlPlane = []string{
"templates/namespace.yaml",
"templates/identity-rbac.yaml",
"templates/destination-rbac.yaml",
"templates/heartbeat-rbac.yaml",
"templates/proxy-injector-rbac.yaml",
"templates/psp.yaml",
"templates/config.yaml",
"templates/identity.yaml",
"templates/destination.yaml",
"templates/heartbeat.yaml",
"templates/proxy-injector.yaml",
}
ignoreCluster bool
)
/* Commands */
func newCmdInstall() *cobra.Command {
values, err := l5dcharts.NewValues()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
var crds bool
var options valuespkg.Options
installOnlyFlags, installOnlyFlagSet := makeInstallFlags(values)
installUpgradeFlags, installUpgradeFlagSet, err := makeInstallUpgradeFlags(values)
if err != nil {
fmt.Fprint(os.Stderr, err.Error())
os.Exit(1)
}
proxyFlags, proxyFlagSet := makeProxyFlags(values)
flags := flattenFlags(installOnlyFlags, installUpgradeFlags, proxyFlags)
cmd := &cobra.Command{
Use: "install [flags]",
Args: cobra.NoArgs,
Short: "Output Kubernetes configs to install Linkerd",
Long: `Output Kubernetes configs to install Linkerd.
This command provides all Kubernetes configs necessary to install the Linkerd
control plane.`,
Example: ` # Install CRDs first.
linkerd install --crds | kubectl apply -f -
# Install the core control plane.
linkerd install | kubectl apply -f -
The installation 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://www.github.com/linkerd/linkerd2/tree/main/charts/linkerd2/README.md`,
RunE: func(cmd *cobra.Command, args []string) error {
var k8sAPI *k8s.KubernetesAPI
if !ignoreCluster {
// Ensure k8s is reachable
if err := errAfterRunningChecks(values.CNIEnabled); err != nil {
fmt.Fprintf(os.Stderr, errMsgCannotInitializeClient, err)
os.Exit(1)
}
// Ensure there is not already an existing Linkerd installation.
k8sAPI, err = k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 30*time.Second)
if err != nil {
return err
}
if !crds {
crds := bytes.Buffer{}
err := renderCRDs(&crds, options)
if err != nil {
fmt.Fprintf(os.Stderr, "%q", err)
os.Exit(1)
}
err = healthcheck.CheckCustomResourceDefinitions(cmd.Context(), k8sAPI, crds.String())
if err != nil {
fmt.Fprintln(os.Stderr, "Linkerd CRDs must be installed first. Run linkerd install with the --crds flag.")
os.Exit(1)
}
}
}
if crds {
// The CRD chart is not configurable.
// TODO(ver): Error if values have been configured?
if err = installCRDs(cmd.Context(), k8sAPI, os.Stdout, options); err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Rendering Linkerd CRDs...")
fmt.Fprintln(os.Stderr, "Next, run `linkerd install | kubectl apply -f -` to install the control plane.")
fmt.Fprintln(os.Stderr)
return nil
}
return installControlPlane(cmd.Context(), k8sAPI, os.Stdout, values, flags, options)
},
}
cmd.Flags().AddFlagSet(installOnlyFlagSet)
cmd.Flags().AddFlagSet(installUpgradeFlagSet)
cmd.Flags().AddFlagSet(proxyFlagSet)
cmd.Flags().BoolVar(&crds, "crds", false, "Install Linkerd CRDs")
cmd.PersistentFlags().BoolVar(&ignoreCluster, "ignore-cluster", false,
"Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)")
flagspkg.AddValueOptionsFlags(cmd.Flags(), &options)
return cmd
}
func checkNoConfig(ctx context.Context, k8sAPI *k8s.KubernetesAPI) error {
if k8sAPI == nil {
// When `ingoreCluster` is set, there is no k8sAPI.
return nil
}
// We just want to check if `linkerd-configmap` exists
_, err := k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{})
if err == nil {
fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, "ConfigMap/linkerd-config already exists")
os.Exit(1)
}
if !kerrors.IsNotFound(err) {
return err
}
return nil
}
func installCRDs(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Writer, options valuespkg.Options) error {
if err := checkNoConfig(ctx, k8sAPI); err != nil {
return err
}
return renderCRDs(w, options)
}
func installControlPlane(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Writer, values *l5dcharts.Values, flags []flag.Flag, options valuespkg.Options) error {
err := flag.ApplySetFlags(values, flags)
if err != nil {
return err
}
if err := checkNoConfig(ctx, k8sAPI); err != nil {
return err
}
// Create values override
valuesOverrides, err := options.MergeValues(nil)
if err != nil {
return err
}
if k8sAPI != nil {
// We just want to check if `linkerd-configmap` exists
_, err = k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{})
if err == nil {
fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, "ConfigMap/linkerd-config already exists")
os.Exit(1)
}
if !kerrors.IsNotFound(err) {
return err
}
if !isRunAsRoot(valuesOverrides) {
err = healthcheck.CheckNodesHaveNonDockerRuntime(ctx, k8sAPI)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
err = initializeIssuerCredentials(ctx, k8sAPI, values)
if err != nil {
return err
}
err = validateValues(ctx, k8sAPI, values)
if err != nil {
return err
}
return renderControlPlane(w, values, valuesOverrides)
}
func isRunAsRoot(values map[string]interface{}) bool {
if proxyInit, ok := values["proxyInit"]; ok {
if val, ok := proxyInit.(map[string]interface{})["runAsRoot"]; ok {
if truth, ok := template.IsTrue(val); ok {
return truth
}
}
}
return false
}
// renderChartToBuffer takes a slice of loaded template files and configuration values and renders
// them into a buffer. The coalesced values are also returned so that they may be rendered via
// `renderOverrides` if appropriate.
func renderChartToBuffer(files []*loader.BufferedFile, values map[string]interface{}, valuesOverrides map[string]interface{}) (*bytes.Buffer, chartutil.Values, error) {
// Load the partials in addition to the main chart.
var partials []*loader.BufferedFile
for _, template := range charts.L5dPartials {
partials = append(partials, &loader.BufferedFile{Name: template})
}
if err := charts.FilesReader(static.Templates, "", partials); err != nil {
return nil, nil, err
}
chart, err := loader.LoadFiles(append(files, partials...))
if err != nil {
return nil, nil, err
}
// Store final Values generated from values.yaml and CLI flags
chart.Values = values
vals, err := chartutil.CoalesceValues(chart, valuesOverrides)
if err != nil {
return nil, nil, err
}
fullValues := map[string]interface{}{
"Values": vals,
"Release": map[string]interface{}{
"Namespace": controlPlaneNamespace,
"Service": "CLI",
},
}
// Attach the final values into the `Values` field for rendering to work
renderedTemplates, err := engine.Render(chart, fullValues)
if err != nil {
return nil, nil, fmt.Errorf("failed to render the template: %w", err)
}
// Merge templates and inject
var buf bytes.Buffer
for _, tmpl := range chart.Templates {
t := path.Join(chart.Metadata.Name, tmpl.Name)
if _, err := buf.WriteString(renderedTemplates[t]); err != nil {
return nil, nil, err
}
}
return &buf, vals, nil
}
func renderCRDs(w io.Writer, options valuespkg.Options) error {
files := []*loader.BufferedFile{
{Name: chartutil.ChartfileName},
}
for _, template := range templatesCrdFiles {
files = append(files, &loader.BufferedFile{Name: template})
}
if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCrds+"/", files); err != nil {
return err
}
// Load defaults from values.yaml
valuesFile := &loader.BufferedFile{Name: l5dcharts.HelmChartDirCrds + "/values.yaml"}
if err := charts.ReadFile(static.Templates, "/", valuesFile); err != nil {
return err
}
var defaultValues map[string]interface{}
err := yaml.Unmarshal(valuesFile.Data, &defaultValues)
if err != nil {
return err
}
defaultValues["cliVersion"] = k8s.CreatedByAnnotationValue()
// Create values override
valuesOverrides, err := options.MergeValues(nil)
if err != nil {
return err
}
buf, _, err := renderChartToBuffer(files, defaultValues, valuesOverrides)
if err != nil {
return err
}
_, err = w.Write(buf.Bytes())
return err
}
func renderControlPlane(w io.Writer, values *l5dcharts.Values, valuesOverrides map[string]interface{}) error {
files := []*loader.BufferedFile{
{Name: chartutil.ChartfileName},
}
for _, template := range templatesControlPlane {
files = append(files, &loader.BufferedFile{Name: template})
}
if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCP+"/", files); err != nil {
return err
}
valuesMap, err := values.ToMap()
if err != nil {
return err
}
buf, vals, err := renderChartToBuffer(files, valuesMap, valuesOverrides)
if err != nil {
return err
}
overrides, err := renderOverrides(vals, false)
if err != nil {
return err
}
buf.WriteString(yamlSep)
buf.WriteString(string(overrides))
_, err = w.Write(buf.Bytes())
return err
}
// renderOverrides outputs the Secret/linkerd-config-overrides resource which
// contains the subset of the values which have been changed from their defaults.
// This secret is used by the upgrade command the load configuration which was
// specified at install time. Note that if identity issuer credentials were
// supplied to the install command or if they were generated by the install
// command, those credentials will be saved here so that they are preserved
// during upgrade. Note also that this Secret/linkerd-config-overrides
// resource is not part of the Helm chart and will not be present when installing
// with Helm. If stringData is set to true, the secret will be rendered using
// the StringData field instead of the Data field, making the output more
// human readable.
func renderOverrides(values chartutil.Values, stringData bool) ([]byte, error) {
defaults, err := l5dcharts.NewValues()
if err != nil {
return nil, err
}
// Remove unnecessary fields, including fields added by helm's `chartutil.CoalesceValues`
delete(values, "configs")
delete(values, "partials")
overrides, err := tree.Diff(defaults, values)
if err != nil {
return nil, err
}
overridesBytes, err := yaml.Marshal(overrides)
if err != nil {
return nil, err
}
secret := corev1.Secret{
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "linkerd-config-overrides",
Namespace: controlPlaneNamespace,
Labels: map[string]string{
k8s.ControllerNSLabel: controlPlaneNamespace,
},
},
}
if stringData {
secret.StringData = map[string]string{
"linkerd-config-overrides": string(overridesBytes),
}
} else {
secret.Data = map[string][]byte{
"linkerd-config-overrides": overridesBytes,
}
}
bytes, err := yaml.Marshal(secret)
if err != nil {
return nil, err
}
return bytes, nil
}
func errAfterRunningChecks(cniEnabled bool) error {
checks := []healthcheck.CategoryID{
healthcheck.KubernetesAPIChecks,
}
hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
ControlPlaneNamespace: controlPlaneNamespace,
KubeConfig: kubeconfigPath,
Impersonate: impersonate,
ImpersonateGroup: impersonateGroup,
KubeContext: kubeContext,
APIAddr: apiAddr,
CNIEnabled: cniEnabled,
})
var err error
hc.RunChecks(func(result *healthcheck.CheckResult) {
if result.Err != nil {
err = result.Err
}
})
return err
}