mirror of https://github.com/linkerd/linkerd2.git
513 lines
15 KiB
Go
513 lines
15 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"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/healthcheck"
|
|
"github.com/linkerd/linkerd2/pkg/k8s"
|
|
"github.com/linkerd/linkerd2/pkg/tree"
|
|
"github.com/spf13/cobra"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/helm/pkg/chartutil"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
const (
|
|
|
|
// addOnChartsPath is where the linkerd2 add-ons will be present
|
|
addOnChartsPath = "add-ons"
|
|
|
|
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/web-rbac.yaml",
|
|
"templates/serviceprofile-crd.yaml",
|
|
"templates/trafficsplit-crd.yaml",
|
|
"templates/proxy-injector-rbac.yaml",
|
|
"templates/sp-validator-rbac.yaml",
|
|
"templates/tap-rbac.yaml",
|
|
"templates/psp.yaml",
|
|
}
|
|
|
|
templatesControlPlaneStage = []string{
|
|
"templates/_helpers.tpl",
|
|
"templates/config.yaml",
|
|
"templates/identity.yaml",
|
|
"templates/controller.yaml",
|
|
"templates/destination.yaml",
|
|
"templates/heartbeat.yaml",
|
|
"templates/web.yaml",
|
|
"templates/proxy-injector.yaml",
|
|
"templates/sp-validator.yaml",
|
|
"templates/tap.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)
|
|
|
|
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 -`,
|
|
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)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().AddFlagSet(flagSet)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newCmdInstallControlPlane(values *l5dcharts.Values) *cobra.Command {
|
|
var skipChecks bool
|
|
|
|
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 -`,
|
|
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)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().AddFlagSet(allStageFlagSet)
|
|
cmd.Flags().AddFlagSet(installOnlyFlagSet)
|
|
cmd.Flags().AddFlagSet(installUpgradeFlagSet)
|
|
cmd.Flags().AddFlagSet(proxyFlagSet)
|
|
|
|
cmd.Flags().BoolVar(
|
|
&skipChecks, "skip-checks", false,
|
|
`Skip checks for namespace existence`,
|
|
)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newCmdInstall() *cobra.Command {
|
|
values, err := l5dcharts.NewValues(false)
|
|
|
|
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.`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return install(cmd.Context(), os.Stdout, values, flags, "")
|
|
},
|
|
}
|
|
|
|
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))
|
|
|
|
return cmd
|
|
}
|
|
|
|
func install(ctx context.Context, w io.Writer, values *l5dcharts.Values, flags []flag.Flag, stage string) 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)
|
|
}
|
|
|
|
func render(w io.Writer, values *l5dcharts.Values, stage string) error {
|
|
|
|
// Set any global flags if present, common with install and upgrade
|
|
values.GetGlobal().Namespace = controlPlaneNamespace
|
|
|
|
// Render raw values and create chart config
|
|
rawValues, err := yaml.Marshal(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
files := []*chartutil.BufferedFile{
|
|
{Name: chartutil.ChartfileName},
|
|
}
|
|
|
|
addOns, err := l5dcharts.ParseAddOnValues(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize add-on sub-charts
|
|
addOnCharts := make(map[string]*charts.Chart)
|
|
for _, addOn := range addOns {
|
|
addOnCharts[addOn.Name()] = &charts.Chart{
|
|
Name: addOn.Name(),
|
|
Dir: addOnChartsPath + "/" + addOn.Name(),
|
|
Namespace: controlPlaneNamespace,
|
|
RawValues: append(addOn.Values(), rawValues...),
|
|
Files: []*chartutil.BufferedFile{
|
|
{
|
|
Name: chartutil.ChartfileName,
|
|
},
|
|
{
|
|
Name: chartutil.ValuesfileName,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
if stage == "" || stage == configStage {
|
|
for _, template := range templatesConfigStage {
|
|
files = append(files,
|
|
&chartutil.BufferedFile{Name: template},
|
|
)
|
|
}
|
|
|
|
// Fill add-on's sub-charts with config templates
|
|
for _, addOn := range addOns {
|
|
addOnCharts[addOn.Name()].Files = append(addOnCharts[addOn.Name()].Files, addOn.ConfigStageTemplates()...)
|
|
}
|
|
}
|
|
|
|
if stage == "" || stage == controlPlaneStage {
|
|
for _, template := range templatesControlPlaneStage {
|
|
files = append(files,
|
|
&chartutil.BufferedFile{Name: template},
|
|
)
|
|
}
|
|
|
|
// Fill add-on's sub-charts with control-plane templates
|
|
for _, addOn := range addOns {
|
|
addOnCharts[addOn.Name()].Files = append(addOnCharts[addOn.Name()].Files, addOn.ControlPlaneStageTemplates()...)
|
|
}
|
|
|
|
}
|
|
|
|
// TODO refactor to use l5dcharts.LoadChart()
|
|
chart := &charts.Chart{
|
|
Name: helmDefaultChartName,
|
|
Dir: helmDefaultChartDir,
|
|
Namespace: controlPlaneNamespace,
|
|
RawValues: rawValues,
|
|
Files: files,
|
|
}
|
|
buf, err := chart.Render()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, addon := range addOns {
|
|
b, err := addOnCharts[addon.Name()].Render()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := buf.WriteString(b.String()); 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(false)
|
|
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,
|
|
},
|
|
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")
|
|
}
|