package cmd import ( "bytes" "context" "errors" "fmt" "io" "os" "path" "strings" "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 ( configStage = "config" controlPlaneStage = "control-plane" helmDefaultChartName = "linkerd2" helmDefaultChartDir = "linkerd2" 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.` errMsgGlobalResourcesExist = `Unable to install the Linkerd control plane. It appears that there is an existing installation: %s If you are sure you'd like to have a fresh install, remove these resources with: linkerd install --ignore-cluster | kubectl delete -f - Otherwise, you can use the --ignore-cluster flag to overwrite the existing global resources. ` errMsgLinkerdConfigResourceConflict = "Can't install the Linkerd control plane in the '%s' namespace. Reason: %s.\nIf this is expected, use the --ignore-cluster flag to continue the installation.\n" errMsgGlobalResourcesMissing = "Can't install the Linkerd control plane in the '%s' namespace. The required Linkerd global resources are missing.\nIf this is expected, use the --skip-checks flag to continue the installation.\n" ) var ( templatesConfigStage = []string{ "templates/namespace.yaml", "templates/identity-rbac.yaml", "templates/controller-rbac.yaml", "templates/destination-rbac.yaml", "templates/heartbeat-rbac.yaml", "templates/serviceprofile-crd.yaml", "templates/trafficsplit-crd.yaml", "templates/proxy-injector-rbac.yaml", "templates/sp-validator-rbac.yaml", "templates/psp.yaml", } templatesControlPlaneStage = []string{ "templates/config.yaml", "templates/identity.yaml", "templates/controller.yaml", "templates/destination.yaml", "templates/heartbeat.yaml", "templates/proxy-injector.yaml", "templates/sp-validator.yaml", } ignoreCluster bool ) /* Commands */ /* The install commands all follow the same flow: * 1. Load default values from the Linkerd2 chart * 2. Apply flags to modify the values * 3. Render the chart using those values * * The individual commands (install, install config, and install control-plane) * differ in which flags are available to each, what pre-check validations * are done, and which subset of the chart is rendered. */ func newCmdInstallConfig(values *l5dcharts.Values) *cobra.Command { flags, flagSet := makeAllStageFlags(values) var options valuespkg.Options cmd := &cobra.Command{ Use: "config [flags]", Args: cobra.NoArgs, Short: "Output Kubernetes cluster-wide resources to install Linkerd", Long: `Output Kubernetes cluster-wide resources to install Linkerd. This command provides Kubernetes configs necessary to install cluster-wide resources for the Linkerd control plane. This command should be followed by "linkerd install control-plane".`, Example: ` # Default install. linkerd install config | kubectl apply -f - # Install Linkerd into a non-default namespace. linkerd install config -L linkerdtest | 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 { err := flag.ApplySetFlags(values, flags) if err != nil { return err } if !ignoreCluster { // Ensure k8s is reachable and that Linkerd is not already installed. if err := errAfterRunningChecks(values.GetGlobal().CNIEnabled); err != nil { if healthcheck.IsCategoryError(err, healthcheck.KubernetesAPIChecks) { fmt.Fprintf(os.Stderr, errMsgCannotInitializeClient, err) } else { fmt.Fprintf(os.Stderr, errMsgGlobalResourcesExist, err) } os.Exit(1) } } return render(os.Stdout, values, configStage, options) }, } flagspkg.AddValueOptionsFlags(cmd.Flags(), &options) cmd.Flags().AddFlagSet(flagSet) return cmd } func newCmdInstallControlPlane(values *l5dcharts.Values) *cobra.Command { var skipChecks bool var options valuespkg.Options allStageFlags, allStageFlagSet := makeAllStageFlags(values) 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(allStageFlags, installOnlyFlags, installUpgradeFlags, proxyFlags) cmd := &cobra.Command{ Use: "control-plane [flags]", Args: cobra.NoArgs, Short: "Output Kubernetes control plane resources to install Linkerd", Long: `Output Kubernetes control plane resources to install Linkerd. This command provides Kubernetes configs necessary to install the Linkerd control plane. It should be run after "linkerd install config".`, Example: ` # Default install. linkerd install control-plane | kubectl apply -f - # Install Linkerd into a non-default namespace. linkerd install control-plane -l linkerdtest | 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 { if !skipChecks { // check if global resources exist to determine if the `install config` // stage succeeded if err := errAfterRunningChecks(values.GetGlobal().CNIEnabled); err == nil { if healthcheck.IsCategoryError(err, healthcheck.KubernetesAPIChecks) { fmt.Fprintf(os.Stderr, errMsgCannotInitializeClient, err) } else { fmt.Fprintf(os.Stderr, errMsgGlobalResourcesMissing, controlPlaneNamespace) } os.Exit(1) } } if !ignoreCluster { // Ensure there is not already an existing Linkerd installation. if err := errIfLinkerdConfigConfigMapExists(cmd.Context()); err != nil { fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, err.Error()) os.Exit(1) } } return install(cmd.Context(), os.Stdout, values, flags, controlPlaneStage, options) }, } cmd.Flags().AddFlagSet(allStageFlagSet) cmd.Flags().AddFlagSet(installOnlyFlagSet) cmd.Flags().AddFlagSet(installUpgradeFlagSet) cmd.Flags().AddFlagSet(proxyFlagSet) flagspkg.AddValueOptionsFlags(cmd.Flags(), &options) cmd.Flags().BoolVar( &skipChecks, "skip-checks", false, `Skip checks for namespace existence`, ) return cmd } func newCmdInstall() *cobra.Command { values, err := l5dcharts.NewValues() var options valuespkg.Options allStageFlags, allStageFlagSet := makeAllStageFlags(values) 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(allStageFlags, 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: ` # Default install. linkerd install | kubectl apply -f - # Install Linkerd into a non-default namespace. linkerd install -l linkerdtest | kubectl apply -f - # Installation may also be broken up into two stages by user privilege, via # subcommands. 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 { return install(cmd.Context(), os.Stdout, values, flags, "", options) }, } cmd.Flags().AddFlagSet(allStageFlagSet) cmd.Flags().AddFlagSet(installOnlyFlagSet) cmd.Flags().AddFlagSet(installUpgradeFlagSet) cmd.Flags().AddFlagSet(proxyFlagSet) cmd.PersistentFlags().BoolVar(&ignoreCluster, "ignore-cluster", false, "Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)") cmd.AddCommand(newCmdInstallConfig(values)) cmd.AddCommand(newCmdInstallControlPlane(values)) flagspkg.AddValueOptionsFlags(cmd.Flags(), &options) return cmd } func install(ctx context.Context, w io.Writer, values *l5dcharts.Values, flags []flag.Flag, stage string, options valuespkg.Options) error { err := flag.ApplySetFlags(values, flags) if err != nil { return err } var k8sAPI *k8s.KubernetesAPI if !ignoreCluster { // 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 } stored, err := loadStoredValues(ctx, k8sAPI) if err != nil { return err } if stored != nil { fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, "Secret/linkerd-config-overrides already exists") os.Exit(1) } } err = initializeIssuerCredentials(ctx, k8sAPI, values) if err != nil { return err } err = validateValues(ctx, k8sAPI, values) if err != nil { return err } return render(w, values, stage, options) } func render(w io.Writer, values *l5dcharts.Values, stage string, options valuespkg.Options) error { // Set any global flags if present, common with install and upgrade values.GetGlobal().Namespace = controlPlaneNamespace values.Stage = stage // Render raw values rawValues, err := yaml.Marshal(values) if err != nil { return err } files := []*loader.BufferedFile{ {Name: chartutil.ChartfileName}, } if stage == "" || stage == configStage { for _, template := range templatesConfigStage { files = append(files, &loader.BufferedFile{Name: template}, ) } } if stage == "" || stage == controlPlaneStage { for _, template := range templatesControlPlaneStage { files = append(files, &loader.BufferedFile{Name: template}, ) } } var partialFiles []*loader.BufferedFile for _, template := range charts.L5dPartials { partialFiles = append(partialFiles, &loader.BufferedFile{Name: template}, ) } // Load all chart files into buffer if err := charts.FilesReader(static.Templates, helmDefaultChartDir+"/", files); err != nil { return err } // Load all partial chart files into buffer if err := charts.FilesReader(static.Templates, "", partialFiles); err != nil { return err } // Create a Chart obj from the files chart, err := loader.LoadFiles(append(files, partialFiles...)) 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 := options.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 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 err } } if stage == "" || stage == controlPlaneStage { overrides, err := renderOverrides(values, values.GetGlobal().Namespace) 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. func renderOverrides(values *l5dcharts.Values, namespace string) ([]byte, error) { defaults, err := l5dcharts.NewValues() if err != nil { return nil, err } values.Configs = l5dcharts.ConfigJSONs{} 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: namespace, Labels: map[string]string{ k8s.ControllerNSLabel: controlPlaneNamespace, }, }, 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, healthcheck.LinkerdPreInstallGlobalResourcesChecks, } hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{ ControlPlaneNamespace: controlPlaneNamespace, KubeConfig: kubeconfigPath, Impersonate: impersonate, ImpersonateGroup: impersonateGroup, KubeContext: kubeContext, APIAddr: apiAddr, CNIEnabled: cniEnabled, }) var k8sAPIError error errMsgs := []string{} hc.RunChecks(func(result *healthcheck.CheckResult) { if result.Err != nil { if ce, ok := result.Err.(*healthcheck.CategoryError); ok { if ce.Category == healthcheck.KubernetesAPIChecks { k8sAPIError = ce } else if re, ok := ce.Err.(*healthcheck.ResourceError); ok { // resource error, print in kind.group/name format for _, res := range re.Resources { errMsgs = append(errMsgs, res.String()) } } else { // unknown category error, just print it errMsgs = append(errMsgs, result.Err.Error()) } } else { // unknown error, just print it errMsgs = append(errMsgs, result.Err.Error()) } } }) // errors from the KubernetesAPIChecks category take precedence if k8sAPIError != nil { return k8sAPIError } if len(errMsgs) > 0 { return errors.New(strings.Join(errMsgs, "\n")) } return nil } func errIfLinkerdConfigConfigMapExists(ctx context.Context) error { kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0) if err != nil { return err } _, err = kubeAPI.CoreV1().Namespaces().Get(ctx, controlPlaneNamespace, metav1.GetOptions{}) if err != nil { return err } _, _, err = healthcheck.FetchCurrentConfiguration(ctx, kubeAPI, controlPlaneNamespace) if err != nil { if kerrors.IsNotFound(err) { return nil } return err } return fmt.Errorf("'linkerd-config' config map already exists") }