mirror of https://github.com/linkerd/linkerd2.git
1093 lines
37 KiB
Go
1093 lines
37 KiB
Go
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/ptypes"
|
|
pb "github.com/linkerd/linkerd2/controller/gen/config"
|
|
"github.com/linkerd/linkerd2/pkg/charts"
|
|
l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
|
|
"github.com/linkerd/linkerd2/pkg/config"
|
|
"github.com/linkerd/linkerd2/pkg/healthcheck"
|
|
"github.com/linkerd/linkerd2/pkg/issuercerts"
|
|
"github.com/linkerd/linkerd2/pkg/k8s"
|
|
consts "github.com/linkerd/linkerd2/pkg/k8s"
|
|
"github.com/linkerd/linkerd2/pkg/tls"
|
|
"github.com/linkerd/linkerd2/pkg/version"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"k8s.io/helm/pkg/chartutil"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
type (
|
|
// installOptions holds values for command line flags that apply to the install
|
|
// command. All fields in this struct should have corresponding flags added in
|
|
// the newCmdInstall func later in this file. It also embeds proxyConfigOptions
|
|
// in order to hold values for command line flags that apply to both inject and
|
|
// install.
|
|
installOptions struct {
|
|
clusterDomain string
|
|
controlPlaneVersion string
|
|
controllerReplicas uint
|
|
controllerLogLevel string
|
|
highAvailability bool
|
|
controllerUID int64
|
|
disableH2Upgrade bool
|
|
disableHeartbeat bool
|
|
noInitContainer bool
|
|
skipChecks bool
|
|
omitWebhookSideEffects bool
|
|
restrictDashboardPrivileges bool
|
|
controlPlaneTracing bool
|
|
identityOptions *installIdentityOptions
|
|
*proxyConfigOptions
|
|
|
|
recordedFlags []*pb.Install_Flag
|
|
|
|
// function pointers that can be overridden for tests
|
|
heartbeatSchedule func() string
|
|
}
|
|
|
|
installIdentityOptions struct {
|
|
replicas uint
|
|
trustDomain string
|
|
|
|
issuanceLifetime time.Duration
|
|
clockSkewAllowance time.Duration
|
|
|
|
trustPEMFile, crtPEMFile, keyPEMFile string
|
|
identityExternalIssuer bool
|
|
}
|
|
)
|
|
|
|
const (
|
|
configStage = "config"
|
|
controlPlaneStage = "control-plane"
|
|
|
|
defaultIdentityIssuanceLifetime = 24 * time.Hour
|
|
defaultIdentityClockSkewAllowance = 20 * time.Second
|
|
|
|
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/prometheus-rbac.yaml",
|
|
"templates/grafana-rbac.yaml",
|
|
"templates/proxy-injector-rbac.yaml",
|
|
"templates/sp-validator-rbac.yaml",
|
|
"templates/tap-rbac.yaml",
|
|
"templates/psp.yaml",
|
|
}
|
|
|
|
templatesControlPlaneStage = []string{
|
|
"templates/_validate.tpl",
|
|
"templates/_affinity.tpl",
|
|
"templates/_config.tpl",
|
|
"templates/_helpers.tpl",
|
|
"templates/_nodeselector.tpl",
|
|
"templates/config.yaml",
|
|
"templates/identity.yaml",
|
|
"templates/controller.yaml",
|
|
"templates/destination.yaml",
|
|
"templates/heartbeat.yaml",
|
|
"templates/web.yaml",
|
|
"templates/prometheus.yaml",
|
|
"templates/grafana.yaml",
|
|
"templates/proxy-injector.yaml",
|
|
"templates/sp-validator.yaml",
|
|
"templates/tap.yaml",
|
|
}
|
|
)
|
|
|
|
// newInstallOptionsWithDefaults initializes install options with default
|
|
// control plane and proxy options. These defaults are read from the Helm
|
|
// values.yaml and values-ha.yaml files.
|
|
//
|
|
// These options may be overridden on the CLI at install-time and will be
|
|
// persisted in Linkerd's control plane configuration to be used at
|
|
// injection-time.
|
|
func newInstallOptionsWithDefaults() (*installOptions, error) {
|
|
defaults, err := l5dcharts.NewValues(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
issuanceLifetime, err := time.ParseDuration(defaults.Identity.Issuer.IssuanceLifetime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clockSkewAllowance, err := time.ParseDuration(defaults.Identity.Issuer.ClockSkewAllowance)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &installOptions{
|
|
clusterDomain: defaults.ClusterDomain,
|
|
controlPlaneVersion: version.Version,
|
|
controllerReplicas: defaults.ControllerReplicas,
|
|
controllerLogLevel: defaults.ControllerLogLevel,
|
|
highAvailability: defaults.HighAvailability,
|
|
controllerUID: defaults.ControllerUID,
|
|
disableH2Upgrade: !defaults.EnableH2Upgrade,
|
|
disableHeartbeat: defaults.DisableHeartBeat,
|
|
noInitContainer: defaults.NoInitContainer,
|
|
omitWebhookSideEffects: defaults.OmitWebhookSideEffects,
|
|
restrictDashboardPrivileges: defaults.RestrictDashboardPrivileges,
|
|
controlPlaneTracing: defaults.ControlPlaneTracing,
|
|
proxyConfigOptions: &proxyConfigOptions{
|
|
proxyVersion: version.Version,
|
|
ignoreCluster: false,
|
|
proxyImage: defaults.Proxy.Image.Name,
|
|
initImage: defaults.ProxyInit.Image.Name,
|
|
initImageVersion: version.ProxyInitVersion,
|
|
dockerRegistry: defaultDockerRegistry,
|
|
imagePullPolicy: defaults.ImagePullPolicy,
|
|
ignoreInboundPorts: nil,
|
|
ignoreOutboundPorts: nil,
|
|
proxyUID: defaults.Proxy.UID,
|
|
proxyLogLevel: defaults.Proxy.LogLevel,
|
|
proxyControlPort: uint(defaults.Proxy.Ports.Control),
|
|
proxyAdminPort: uint(defaults.Proxy.Ports.Admin),
|
|
proxyInboundPort: uint(defaults.Proxy.Ports.Inbound),
|
|
proxyOutboundPort: uint(defaults.Proxy.Ports.Outbound),
|
|
proxyCPURequest: defaults.Proxy.Resources.CPU.Request,
|
|
proxyMemoryRequest: defaults.Proxy.Resources.Memory.Request,
|
|
proxyCPULimit: defaults.Proxy.Resources.CPU.Limit,
|
|
proxyMemoryLimit: defaults.Proxy.Resources.Memory.Limit,
|
|
enableExternalProfiles: defaults.Proxy.EnableExternalProfiles,
|
|
},
|
|
identityOptions: &installIdentityOptions{
|
|
trustDomain: defaults.Identity.TrustDomain,
|
|
issuanceLifetime: issuanceLifetime,
|
|
clockSkewAllowance: clockSkewAllowance,
|
|
identityExternalIssuer: false,
|
|
},
|
|
|
|
heartbeatSchedule: func() string {
|
|
// Some of the heartbeat Prometheus queries rely on 5m resolution, which
|
|
// means at least 5 minutes of data available. Start the first CronJob 10
|
|
// minutes after `linkerd install` is run, to give the user 5 minutes to
|
|
// install.
|
|
t := time.Now().Add(10 * time.Minute).UTC()
|
|
return fmt.Sprintf("%d %d * * * ", t.Minute(), t.Hour())
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Flag configuration matrix
|
|
//
|
|
// | recordableFlagSet | allStageFlagSet | installOnlyFlagSet | installPersistentFlagSet | upgradeOnlyFlagSet | "skip-checks" |
|
|
// `linkerd install` | X | X | X | X | | |
|
|
// `linkerd install config` | | X | | X | | |
|
|
// `linkerd install control-plane` | X | X | X | X | | X |
|
|
// `linkerd upgrade` | X | X | | | X | |
|
|
// `linkerd upgrade config` | | X | | | | |
|
|
// `linkerd upgrade control-plane` | X | X | | | X | |
|
|
//
|
|
// allStageFlagSet is a subset of recordableFlagSet, but is also added to `linkerd [install|upgrade] config`
|
|
// proxyConfigOptions.flagSet is a subset of recordableFlagSet, and is used by `linkerd inject`.
|
|
|
|
// newCmdInstallConfig is a subcommand for `linkerd install config`
|
|
func newCmdInstallConfig(options *installOptions, parentFlags *pflag.FlagSet) *cobra.Command {
|
|
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 {
|
|
if !options.ignoreCluster {
|
|
if err := errAfterRunningChecks(options); 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 installRunE(options, configStage, parentFlags)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().AddFlagSet(options.allStageFlagSet())
|
|
|
|
return cmd
|
|
}
|
|
|
|
// newCmdInstallControlPlane is a subcommand for `linkerd install control-plane`
|
|
func newCmdInstallControlPlane(options *installOptions) *cobra.Command {
|
|
// The base flags are recorded separately so that they can be serialized into
|
|
// the configuration in validateAndBuild.
|
|
flags := options.recordableFlagSet()
|
|
installOnlyFlags := options.installOnlyFlagSet()
|
|
|
|
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 !options.skipChecks {
|
|
// check if global resources exist to determine if the `install config`
|
|
// stage succeeded
|
|
if err := errAfterRunningChecks(options); 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 !options.ignoreCluster {
|
|
if err := errIfLinkerdConfigConfigMapExists(); err != nil {
|
|
fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
}
|
|
|
|
return installRunE(options, controlPlaneStage, flags)
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().BoolVar(
|
|
&options.skipChecks, "skip-checks", options.skipChecks,
|
|
`Skip checks for namespace existence`,
|
|
)
|
|
cmd.PersistentFlags().AddFlagSet(flags)
|
|
// Some flags are not available during upgrade, etc.
|
|
cmd.PersistentFlags().AddFlagSet(installOnlyFlags)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newCmdInstall() *cobra.Command {
|
|
options, err := newInstallOptionsWithDefaults()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// The base flags are recorded separately so that they can be serialized into
|
|
// the configuration in validateAndBuild.
|
|
flags := options.recordableFlagSet()
|
|
installOnlyFlags := options.installOnlyFlagSet()
|
|
installPersistentFlags := options.installPersistentFlagSet()
|
|
|
|
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 {
|
|
if !options.ignoreCluster {
|
|
if err := errAfterRunningChecks(options); 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 installRunE(options, "", flags)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().AddFlagSet(flags)
|
|
|
|
// Some flags are not available during upgrade, etc.
|
|
cmd.Flags().AddFlagSet(installOnlyFlags)
|
|
cmd.PersistentFlags().AddFlagSet(installPersistentFlags)
|
|
|
|
cmd.AddCommand(newCmdInstallConfig(options, flags))
|
|
cmd.AddCommand(newCmdInstallControlPlane(options))
|
|
|
|
return cmd
|
|
}
|
|
|
|
func installRunE(options *installOptions, stage string, flags *pflag.FlagSet) error {
|
|
values, _, err := options.validateAndBuild(stage, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return render(os.Stdout, values)
|
|
}
|
|
|
|
func (options *installOptions) validateAndBuild(stage string, flags *pflag.FlagSet) (*l5dcharts.Values, *pb.All, error) {
|
|
if err := options.validate(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
options.recordFlags(flags)
|
|
|
|
identityValues, err := options.identityOptions.validateAndBuild()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
configs := options.configs(toIdentityContext(identityValues))
|
|
|
|
values, err := options.buildValuesWithoutIdentity(configs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
values.Identity = identityValues
|
|
values.Stage = stage
|
|
|
|
return values, configs, nil
|
|
}
|
|
|
|
// recordableFlagSet returns flags usable during install or upgrade.
|
|
func (options *installOptions) recordableFlagSet() *pflag.FlagSet {
|
|
e := pflag.ExitOnError
|
|
|
|
flags := pflag.NewFlagSet("install", e)
|
|
|
|
flags.AddFlagSet(options.proxyConfigOptions.flagSet(e))
|
|
flags.AddFlagSet(options.allStageFlagSet())
|
|
|
|
flags.UintVar(
|
|
&options.controllerReplicas, "controller-replicas", options.controllerReplicas,
|
|
"Replicas of the controller to deploy",
|
|
)
|
|
|
|
flags.StringVar(
|
|
&options.controllerLogLevel, "controller-log-level", options.controllerLogLevel,
|
|
"Log level for the controller and web components",
|
|
)
|
|
flags.BoolVar(
|
|
&options.highAvailability, "ha", options.highAvailability,
|
|
"Enable HA deployment config for the control plane (default false)",
|
|
)
|
|
flags.Int64Var(
|
|
&options.controllerUID, "controller-uid", options.controllerUID,
|
|
"Run the control plane components under this user ID",
|
|
)
|
|
flags.BoolVar(
|
|
&options.disableH2Upgrade, "disable-h2-upgrade", options.disableH2Upgrade,
|
|
"Prevents the controller from instructing proxies to perform transparent HTTP/2 upgrading (default false)",
|
|
)
|
|
flags.BoolVar(
|
|
&options.disableHeartbeat, "disable-heartbeat", options.disableHeartbeat,
|
|
"Disables the heartbeat cronjob (default false)",
|
|
)
|
|
flags.DurationVar(
|
|
&options.identityOptions.issuanceLifetime, "identity-issuance-lifetime", options.identityOptions.issuanceLifetime,
|
|
"The amount of time for which the Identity issuer should certify identity",
|
|
)
|
|
flags.DurationVar(
|
|
&options.identityOptions.clockSkewAllowance, "identity-clock-skew-allowance", options.identityOptions.clockSkewAllowance,
|
|
"The amount of time to allow for clock skew within a Linkerd cluster",
|
|
)
|
|
flags.BoolVar(
|
|
&options.omitWebhookSideEffects, "omit-webhook-side-effects", options.omitWebhookSideEffects,
|
|
"Omit the sideEffects flag in the webhook manifests, This flag must be provided during install or upgrade for Kubernetes versions pre 1.12",
|
|
)
|
|
flags.BoolVar(
|
|
&options.controlPlaneTracing, "control-plane-tracing", options.controlPlaneTracing,
|
|
"Enables Control Plane Tracing with the defaults",
|
|
)
|
|
flags.StringVar(
|
|
&options.identityOptions.crtPEMFile, "identity-issuer-certificate-file", options.identityOptions.crtPEMFile,
|
|
"A path to a PEM-encoded file containing the Linkerd Identity issuer certificate (generated by default)",
|
|
)
|
|
flags.StringVar(
|
|
&options.identityOptions.keyPEMFile, "identity-issuer-key-file", options.identityOptions.keyPEMFile,
|
|
"A path to a PEM-encoded file containing the Linkerd Identity issuer private key (generated by default)",
|
|
)
|
|
flags.StringVar(
|
|
&options.identityOptions.trustPEMFile, "identity-trust-anchors-file", options.identityOptions.trustPEMFile,
|
|
"A path to a PEM-encoded file containing Linkerd Identity trust anchors (generated by default)",
|
|
)
|
|
flags.StringVarP(&options.controlPlaneVersion, "control-plane-version", "", options.controlPlaneVersion, "(Development) Tag to be used for the control plane component images")
|
|
flags.MarkHidden("control-plane-version")
|
|
flags.MarkHidden("control-plane-tracing")
|
|
|
|
return flags
|
|
}
|
|
|
|
// allStageFlagSet returns flags usable for single and multi-stage installs and
|
|
// upgrades. For multi-stage installs, users must set these flags consistently
|
|
// across commands.
|
|
func (options *installOptions) allStageFlagSet() *pflag.FlagSet {
|
|
flags := pflag.NewFlagSet("all-stage", pflag.ExitOnError)
|
|
|
|
flags.BoolVar(&options.noInitContainer, "linkerd-cni-enabled", options.noInitContainer,
|
|
"Experimental: Omit the NET_ADMIN capability in the PSP and the proxy-init container when injecting the proxy; requires the linkerd-cni plugin to already be installed",
|
|
)
|
|
|
|
flags.BoolVar(
|
|
&options.restrictDashboardPrivileges, "restrict-dashboard-privileges", options.restrictDashboardPrivileges,
|
|
"Restrict the Linkerd Dashboard's default privileges to disallow Tap and Check",
|
|
)
|
|
return flags
|
|
}
|
|
|
|
// installOnlyFlagSet includes flags that are only accessible at install-time
|
|
// and not at upgrade-time.
|
|
func (options *installOptions) installOnlyFlagSet() *pflag.FlagSet {
|
|
flags := pflag.NewFlagSet("install-only", pflag.ExitOnError)
|
|
|
|
flags.StringVar(
|
|
&options.clusterDomain, "cluster-domain", options.clusterDomain,
|
|
"Set custom cluster domain",
|
|
)
|
|
flags.StringVar(
|
|
&options.identityOptions.trustDomain, "identity-trust-domain", options.identityOptions.trustDomain,
|
|
"Configures the name suffix used for identities.",
|
|
)
|
|
flags.BoolVar(
|
|
&options.identityOptions.identityExternalIssuer, "identity-external-issuer", options.identityOptions.identityExternalIssuer,
|
|
"Whether to use an external identity issuer (default false)",
|
|
)
|
|
return flags
|
|
}
|
|
|
|
// installPersistentFlagSet includes flags that are only accessible at
|
|
// install-time, not at upgrade-time, and are also used by install subcommands.
|
|
func (options *installOptions) installPersistentFlagSet() *pflag.FlagSet {
|
|
flags := pflag.NewFlagSet("install-persist", pflag.ExitOnError)
|
|
|
|
flags.BoolVar(
|
|
&options.ignoreCluster, "ignore-cluster", options.ignoreCluster,
|
|
"Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)",
|
|
)
|
|
|
|
return flags
|
|
}
|
|
|
|
func (options *installOptions) recordFlags(flags *pflag.FlagSet) {
|
|
if flags == nil {
|
|
return
|
|
}
|
|
|
|
flags.VisitAll(func(f *pflag.Flag) {
|
|
if f.Changed {
|
|
switch f.Name {
|
|
case "ignore-cluster", "control-plane-version", "proxy-version", "identity-issuer-certificate-file", "identity-issuer-key-file", "identity-trust-anchors-file":
|
|
// These flags don't make sense to record.
|
|
default:
|
|
options.recordedFlags = append(options.recordedFlags, &pb.Install_Flag{
|
|
Name: f.Name,
|
|
Value: f.Value.String(),
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (options *installOptions) validate() error {
|
|
if options.ignoreCluster && options.identityOptions.identityExternalIssuer {
|
|
return errors.New("--ignore-cluster is not supported when --identity-external-issuer=true")
|
|
}
|
|
|
|
if options.controlPlaneVersion != "" && !alphaNumDashDot.MatchString(options.controlPlaneVersion) {
|
|
return fmt.Errorf("%s is not a valid version", options.controlPlaneVersion)
|
|
}
|
|
|
|
if options.identityOptions == nil {
|
|
// Programmer error: identityOptions may be empty, but it must be set by the constructor.
|
|
panic("missing identity options")
|
|
}
|
|
|
|
if _, err := log.ParseLevel(options.controllerLogLevel); err != nil {
|
|
return fmt.Errorf("--controller-log-level must be one of: panic, fatal, error, warn, info, debug")
|
|
}
|
|
|
|
if err := options.proxyConfigOptions.validate(); err != nil {
|
|
return err
|
|
}
|
|
if options.proxyLogLevel == "" {
|
|
return errors.New("--proxy-log-level must not be empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildValuesWithoutIdentity builds the values that will be used to render
|
|
// the Helm templates. It overrides the defaults values with CLI options.
|
|
func (options *installOptions) buildValuesWithoutIdentity(configs *pb.All) (*l5dcharts.Values, error) {
|
|
installValues, err := l5dcharts.NewValues(options.highAvailability)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if options.highAvailability {
|
|
// use the HA defaults if CLI options aren't provided
|
|
if options.controllerReplicas == 1 {
|
|
options.controllerReplicas = installValues.ControllerReplicas
|
|
}
|
|
|
|
if options.proxyCPURequest == "" {
|
|
options.proxyCPURequest = installValues.Proxy.Resources.CPU.Request
|
|
}
|
|
|
|
if options.proxyMemoryRequest == "" {
|
|
options.proxyMemoryRequest = installValues.Proxy.Resources.Memory.Request
|
|
}
|
|
|
|
if options.proxyCPULimit == "" {
|
|
options.proxyCPULimit = installValues.Proxy.Resources.CPU.Limit
|
|
}
|
|
|
|
if options.proxyMemoryLimit == "" {
|
|
options.proxyMemoryLimit = installValues.Proxy.Resources.Memory.Limit
|
|
}
|
|
|
|
// `configs` was built before the HA option is evaluated, so we need
|
|
// to make sure the HA proxy resources are added here.
|
|
if configs.Proxy.Resource.RequestCpu == "" {
|
|
configs.Proxy.Resource.RequestCpu = options.proxyCPURequest
|
|
}
|
|
|
|
if configs.Proxy.Resource.RequestMemory == "" {
|
|
configs.Proxy.Resource.RequestMemory = options.proxyMemoryRequest
|
|
}
|
|
|
|
if configs.Proxy.Resource.LimitCpu == "" {
|
|
configs.Proxy.Resource.LimitCpu = options.proxyCPULimit
|
|
}
|
|
|
|
if configs.Proxy.Resource.LimitMemory == "" {
|
|
configs.Proxy.Resource.LimitMemory = options.proxyMemoryLimit
|
|
}
|
|
|
|
options.identityOptions.replicas = options.controllerReplicas
|
|
}
|
|
|
|
globalJSON, proxyJSON, installJSON, err := config.ToJSON(configs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// override default values with CLI options
|
|
installValues.ClusterDomain = configs.GetGlobal().GetClusterDomain()
|
|
installValues.Configs.Global = globalJSON
|
|
installValues.Configs.Proxy = proxyJSON
|
|
installValues.Configs.Install = installJSON
|
|
installValues.ControllerImage = fmt.Sprintf("%s/controller", options.dockerRegistry)
|
|
installValues.ControllerImageVersion = configs.GetGlobal().GetVersion()
|
|
installValues.ControllerLogLevel = options.controllerLogLevel
|
|
installValues.ControllerReplicas = options.controllerReplicas
|
|
installValues.ControllerUID = options.controllerUID
|
|
installValues.ControlPlaneTracing = options.controlPlaneTracing
|
|
installValues.EnableH2Upgrade = !options.disableH2Upgrade
|
|
installValues.EnablePodAntiAffinity = options.highAvailability
|
|
installValues.HighAvailability = options.highAvailability
|
|
installValues.ImagePullPolicy = options.imagePullPolicy
|
|
installValues.GrafanaImage = fmt.Sprintf("%s/grafana", options.dockerRegistry)
|
|
installValues.Namespace = controlPlaneNamespace
|
|
installValues.NoInitContainer = options.noInitContainer
|
|
installValues.OmitWebhookSideEffects = options.omitWebhookSideEffects
|
|
installValues.PrometheusLogLevel = toPromLogLevel(strings.ToLower(options.controllerLogLevel))
|
|
installValues.HeartbeatSchedule = options.heartbeatSchedule()
|
|
installValues.RestrictDashboardPrivileges = options.restrictDashboardPrivileges
|
|
installValues.DisableHeartBeat = options.disableHeartbeat
|
|
installValues.WebImage = fmt.Sprintf("%s/web", options.dockerRegistry)
|
|
|
|
installValues.Proxy = &l5dcharts.Proxy{
|
|
EnableExternalProfiles: options.enableExternalProfiles,
|
|
Image: &l5dcharts.Image{
|
|
Name: registryOverride(options.proxyImage, options.dockerRegistry),
|
|
PullPolicy: options.imagePullPolicy,
|
|
Version: options.proxyVersion,
|
|
},
|
|
LogLevel: options.proxyLogLevel,
|
|
Ports: &l5dcharts.Ports{
|
|
Admin: int32(options.proxyAdminPort),
|
|
Control: int32(options.proxyControlPort),
|
|
Inbound: int32(options.proxyInboundPort),
|
|
Outbound: int32(options.proxyOutboundPort),
|
|
},
|
|
Resources: &l5dcharts.Resources{
|
|
CPU: l5dcharts.Constraints{
|
|
Limit: options.proxyCPULimit,
|
|
Request: options.proxyCPURequest,
|
|
},
|
|
Memory: l5dcharts.Constraints{
|
|
Limit: options.proxyMemoryLimit,
|
|
Request: options.proxyMemoryRequest,
|
|
},
|
|
},
|
|
UID: options.proxyUID,
|
|
}
|
|
|
|
inboundPortStrs := []string{}
|
|
for _, port := range options.ignoreInboundPorts {
|
|
inboundPortStrs = append(inboundPortStrs, strconv.FormatUint(uint64(port), 10))
|
|
}
|
|
outboundPortStrs := []string{}
|
|
for _, port := range options.ignoreOutboundPorts {
|
|
outboundPortStrs = append(outboundPortStrs, strconv.FormatUint(uint64(port), 10))
|
|
}
|
|
|
|
installValues.ProxyInit.Image.Name = registryOverride(options.initImage, options.dockerRegistry)
|
|
installValues.ProxyInit.Image.PullPolicy = options.imagePullPolicy
|
|
installValues.ProxyInit.Image.Version = options.initImageVersion
|
|
installValues.ProxyInit.IgnoreInboundPorts = strings.Join(inboundPortStrs, ",")
|
|
installValues.ProxyInit.IgnoreOutboundPorts = strings.Join(outboundPortStrs, ",")
|
|
|
|
return installValues, nil
|
|
}
|
|
|
|
func toPromLogLevel(level string) string {
|
|
switch level {
|
|
case "panic", "fatal":
|
|
return "error"
|
|
default:
|
|
return level
|
|
}
|
|
}
|
|
|
|
func render(w io.Writer, values *l5dcharts.Values) error {
|
|
// Render raw values and create chart config
|
|
rawValues, err := yaml.Marshal(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
files := []*chartutil.BufferedFile{
|
|
{Name: chartutil.ChartfileName},
|
|
}
|
|
|
|
if values.Stage == "" || values.Stage == configStage {
|
|
for _, template := range templatesConfigStage {
|
|
files = append(files, &chartutil.BufferedFile{
|
|
Name: template,
|
|
})
|
|
}
|
|
}
|
|
|
|
if values.Stage == "" || values.Stage == controlPlaneStage {
|
|
for _, template := range templatesControlPlaneStage {
|
|
files = append(files, &chartutil.BufferedFile{
|
|
Name: template,
|
|
})
|
|
}
|
|
}
|
|
|
|
chart := &charts.Chart{
|
|
Name: helmDefaultChartName,
|
|
Dir: helmDefaultChartDir,
|
|
Namespace: controlPlaneNamespace,
|
|
RawValues: rawValues,
|
|
Files: files,
|
|
}
|
|
buf, err := chart.Render()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = w.Write(buf.Bytes())
|
|
return err
|
|
}
|
|
|
|
func (options *installOptions) configs(identity *pb.IdentityContext) *pb.All {
|
|
return &pb.All{
|
|
Global: options.globalConfig(identity),
|
|
Proxy: options.proxyConfig(),
|
|
Install: options.installConfig(),
|
|
}
|
|
}
|
|
|
|
func (options *installOptions) globalConfig(identity *pb.IdentityContext) *pb.Global {
|
|
return &pb.Global{
|
|
LinkerdNamespace: controlPlaneNamespace,
|
|
CniEnabled: options.noInitContainer,
|
|
Version: options.controlPlaneVersion,
|
|
IdentityContext: identity,
|
|
OmitWebhookSideEffects: options.omitWebhookSideEffects,
|
|
ClusterDomain: options.clusterDomain,
|
|
}
|
|
}
|
|
|
|
func (options *installOptions) installConfig() *pb.Install {
|
|
return &pb.Install{
|
|
CliVersion: version.Version,
|
|
Flags: options.recordedFlags,
|
|
}
|
|
}
|
|
|
|
func (options *installOptions) proxyConfig() *pb.Proxy {
|
|
ignoreInboundPorts := []*pb.Port{}
|
|
for _, port := range options.ignoreInboundPorts {
|
|
ignoreInboundPorts = append(ignoreInboundPorts, &pb.Port{Port: uint32(port)})
|
|
}
|
|
|
|
ignoreOutboundPorts := []*pb.Port{}
|
|
for _, port := range options.ignoreOutboundPorts {
|
|
ignoreOutboundPorts = append(ignoreOutboundPorts, &pb.Port{Port: uint32(port)})
|
|
}
|
|
|
|
return &pb.Proxy{
|
|
ProxyImage: &pb.Image{
|
|
ImageName: registryOverride(options.proxyImage, options.dockerRegistry),
|
|
PullPolicy: options.imagePullPolicy,
|
|
},
|
|
ProxyInitImage: &pb.Image{
|
|
ImageName: registryOverride(options.initImage, options.dockerRegistry),
|
|
PullPolicy: options.imagePullPolicy,
|
|
},
|
|
ControlPort: &pb.Port{
|
|
Port: uint32(options.proxyControlPort),
|
|
},
|
|
IgnoreInboundPorts: ignoreInboundPorts,
|
|
IgnoreOutboundPorts: ignoreOutboundPorts,
|
|
InboundPort: &pb.Port{
|
|
Port: uint32(options.proxyInboundPort),
|
|
},
|
|
AdminPort: &pb.Port{
|
|
Port: uint32(options.proxyAdminPort),
|
|
},
|
|
OutboundPort: &pb.Port{
|
|
Port: uint32(options.proxyOutboundPort),
|
|
},
|
|
Resource: &pb.ResourceRequirements{
|
|
RequestCpu: options.proxyCPURequest,
|
|
RequestMemory: options.proxyMemoryRequest,
|
|
LimitCpu: options.proxyCPULimit,
|
|
LimitMemory: options.proxyMemoryLimit,
|
|
},
|
|
ProxyUid: options.proxyUID,
|
|
LogLevel: &pb.LogLevel{
|
|
Level: options.proxyLogLevel,
|
|
},
|
|
DisableExternalProfiles: !options.enableExternalProfiles,
|
|
ProxyVersion: options.proxyVersion,
|
|
ProxyInitImageVersion: options.initImageVersion,
|
|
}
|
|
}
|
|
|
|
func errAfterRunningChecks(options *installOptions) error {
|
|
checks := []healthcheck.CategoryID{
|
|
healthcheck.KubernetesAPIChecks,
|
|
healthcheck.LinkerdPreInstallGlobalResourcesChecks,
|
|
}
|
|
hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
|
|
ControlPlaneNamespace: controlPlaneNamespace,
|
|
KubeConfig: kubeconfigPath,
|
|
Impersonate: impersonate,
|
|
KubeContext: kubeContext,
|
|
APIAddr: apiAddr,
|
|
NoInitContainer: options.noInitContainer,
|
|
})
|
|
|
|
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() error {
|
|
kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = kubeAPI.CoreV1().Namespaces().Get(controlPlaneNamespace, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = healthcheck.FetchLinkerdConfigMap(kubeAPI, controlPlaneNamespace)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("'linkerd-config' config map already exists")
|
|
}
|
|
|
|
func checkFilesExist(files []string) error {
|
|
for _, f := range files {
|
|
stat, err := os.Stat(f)
|
|
if err != nil {
|
|
return fmt.Errorf("missing file: %s", err)
|
|
}
|
|
if stat.IsDir() {
|
|
return fmt.Errorf("not a file: %s", f)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (idopts *installIdentityOptions) validate() error {
|
|
if idopts == nil {
|
|
return nil
|
|
}
|
|
|
|
if idopts.trustDomain != "" {
|
|
if errs := validation.IsDNS1123Subdomain(idopts.trustDomain); len(errs) > 0 {
|
|
return fmt.Errorf("invalid trust domain '%s': %s", idopts.trustDomain, errs[0])
|
|
}
|
|
}
|
|
|
|
if idopts.identityExternalIssuer {
|
|
|
|
if idopts.crtPEMFile != "" {
|
|
return errors.New("--identity-issuer-certificate-file must not be specified if --identity-external-issuer=true")
|
|
}
|
|
|
|
if idopts.keyPEMFile != "" {
|
|
return errors.New("--identity-issuer-key-file must not be specified if --identity-external-issuer=true")
|
|
}
|
|
|
|
if idopts.trustPEMFile != "" {
|
|
return errors.New("--identity-trust-anchors-file must not be specified if --identity-external-issuer=true")
|
|
}
|
|
|
|
} else {
|
|
if idopts.trustPEMFile != "" || idopts.crtPEMFile != "" || idopts.keyPEMFile != "" {
|
|
if idopts.trustPEMFile == "" {
|
|
return errors.New("a trust anchors file must be specified if other credentials are provided")
|
|
}
|
|
if idopts.crtPEMFile == "" {
|
|
return errors.New("a certificate file must be specified if other credentials are provided")
|
|
}
|
|
if idopts.keyPEMFile == "" {
|
|
return errors.New("a private key file must be specified if other credentials are provided")
|
|
}
|
|
if err := checkFilesExist([]string{idopts.trustPEMFile, idopts.crtPEMFile, idopts.keyPEMFile}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (idopts *installIdentityOptions) validateAndBuild() (*l5dcharts.Identity, error) {
|
|
if idopts == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
if err := idopts.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if idopts.identityExternalIssuer {
|
|
return idopts.readExternallyManaged()
|
|
} else if idopts.trustPEMFile != "" && idopts.crtPEMFile != "" && idopts.keyPEMFile != "" {
|
|
return idopts.readValues()
|
|
} else {
|
|
return idopts.genValues()
|
|
}
|
|
}
|
|
|
|
func (idopts *installIdentityOptions) issuerName() string {
|
|
return fmt.Sprintf("identity.%s.%s", controlPlaneNamespace, idopts.trustDomain)
|
|
}
|
|
|
|
func (idopts *installIdentityOptions) genValues() (*l5dcharts.Identity, error) {
|
|
root, err := tls.GenerateRootCAWithDefaults(idopts.issuerName())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate root certificate for identity: %s", err)
|
|
}
|
|
|
|
return &l5dcharts.Identity{
|
|
TrustDomain: idopts.trustDomain,
|
|
TrustAnchorsPEM: root.Cred.Crt.EncodeCertificatePEM(),
|
|
Issuer: &l5dcharts.Issuer{
|
|
Scheme: consts.IdentityIssuerSchemeLinkerd,
|
|
ClockSkewAllowance: idopts.clockSkewAllowance.String(),
|
|
IssuanceLifetime: idopts.issuanceLifetime.String(),
|
|
CrtExpiry: root.Cred.Crt.Certificate.NotAfter,
|
|
CrtExpiryAnnotation: k8s.IdentityIssuerExpiryAnnotation,
|
|
TLS: &l5dcharts.TLS{
|
|
KeyPEM: root.Cred.EncodePrivateKeyPEM(),
|
|
CrtPEM: root.Cred.Crt.EncodeCertificatePEM(),
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (idopts *installIdentityOptions) readExternallyManaged() (*l5dcharts.Identity, error) {
|
|
|
|
kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error fetching external issuer config: %s", err)
|
|
}
|
|
|
|
externalIssuerData, err := issuercerts.FetchExternalIssuerData(kubeAPI, controlPlaneNamespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = externalIssuerData.VerifyAndBuildCreds(idopts.issuerName())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read CA from %s: %s", consts.IdentityIssuerSecretName, err)
|
|
}
|
|
|
|
return &l5dcharts.Identity{
|
|
TrustDomain: idopts.trustDomain,
|
|
TrustAnchorsPEM: externalIssuerData.TrustAnchors,
|
|
Issuer: &l5dcharts.Issuer{
|
|
Scheme: string(corev1.SecretTypeTLS),
|
|
ClockSkewAllowance: idopts.clockSkewAllowance.String(),
|
|
IssuanceLifetime: idopts.issuanceLifetime.String(),
|
|
},
|
|
}, nil
|
|
|
|
}
|
|
|
|
// readValues attempts to read an issuer configuration from disk
|
|
// to produce an `installIdentityValues`.
|
|
//
|
|
// The identity options must have already been validated.
|
|
func (idopts *installIdentityOptions) readValues() (*l5dcharts.Identity, error) {
|
|
issuerData, err := issuercerts.LoadIssuerDataFromFiles(idopts.keyPEMFile, idopts.crtPEMFile, idopts.trustPEMFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
creds, err := issuerData.VerifyAndBuildCreds(idopts.issuerName())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify issuer certs stored on disk: %s", err)
|
|
}
|
|
|
|
return &l5dcharts.Identity{
|
|
TrustDomain: idopts.trustDomain,
|
|
TrustAnchorsPEM: issuerData.TrustAnchors,
|
|
Issuer: &l5dcharts.Issuer{
|
|
Scheme: consts.IdentityIssuerSchemeLinkerd,
|
|
ClockSkewAllowance: idopts.clockSkewAllowance.String(),
|
|
IssuanceLifetime: idopts.issuanceLifetime.String(),
|
|
CrtExpiry: creds.Crt.Certificate.NotAfter,
|
|
CrtExpiryAnnotation: k8s.IdentityIssuerExpiryAnnotation,
|
|
TLS: &l5dcharts.TLS{
|
|
KeyPEM: creds.EncodePrivateKeyPEM(),
|
|
CrtPEM: creds.EncodeCertificatePEM(),
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func toIdentityContext(idvals *l5dcharts.Identity) *pb.IdentityContext {
|
|
if idvals == nil {
|
|
return nil
|
|
}
|
|
|
|
il, err := time.ParseDuration(idvals.Issuer.IssuanceLifetime)
|
|
if err != nil {
|
|
il = defaultIdentityIssuanceLifetime
|
|
}
|
|
|
|
csa, err := time.ParseDuration(idvals.Issuer.ClockSkewAllowance)
|
|
if err != nil {
|
|
csa = defaultIdentityClockSkewAllowance
|
|
}
|
|
|
|
return &pb.IdentityContext{
|
|
TrustDomain: idvals.TrustDomain,
|
|
TrustAnchorsPem: idvals.TrustAnchorsPEM,
|
|
IssuanceLifetime: ptypes.DurationProto(il),
|
|
ClockSkewAllowance: ptypes.DurationProto(csa),
|
|
Scheme: idvals.Issuer.Scheme,
|
|
}
|
|
}
|