Skip to content

Commit

Permalink
cli: Introduce an upgrade command (linkerd#2564)
Browse files Browse the repository at this point in the history
The `install` command errors when the deploy target contains an existing
Linkerd deployment. The `upgrade` command is introduced to reinstall or
reconfigure the Linkerd control plane.

Upgrade works as follows:

1. The controller config is fetched from the Kubernetes API. The Public
   API is not used, because we need to be able to reinstall the control
   plane when the Public API is not available; and we are not concerned
   about RBAC restrictions preventing the installer from reading the
   config (as we are for inject).

2. The install configuration is read, particularly the flags used during
   the last install/upgrade. If these flags were not set again during the
   upgrade, the previous values are used as if they were passed this time.
   The configuration is updated from the combination of these values,
   including the install configuration itself.

   Note that some flags, including the linkerd-version, are omitted
   since they are stored elsewhere in the configurations and don't make
   sense to track as overrides..

3. The issuer secrets are read from the Kubernetes API so that they can
   be re-used. There is currently no way to reconfigure issuer
   certificates. We will need to create _another_ workflow for
   updating these credentials.

4. The install rendering is invoked with values and config fetched from
   the cluster, synthesized with the new configuration.

Signed-off-by: [email protected] <[email protected]>
  • Loading branch information
olix0r authored and KatherineMelnyk committed Apr 4, 2019
1 parent 521c9cd commit 96cd003
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 75 deletions.
11 changes: 8 additions & 3 deletions cli/cmd/inject.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,16 @@ sub-folders, or coming from stdin.`,
},
}

cmd.PersistentFlags().AddFlagSet(options.proxyConfigOptions.flagSet(pflag.ExitOnError))
cmd.PersistentFlags().BoolVar(
flags := options.proxyConfigOptions.flagSet(pflag.ExitOnError)
flags.BoolVar(
&options.disableIdentity, "disable-identity", options.disableIdentity,
"Disables resources from participating in TLS identity",
)
flags.BoolVar(
&options.ignoreCluster, "ignore-cluster", options.ignoreCluster,
"Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)",
)
cmd.PersistentFlags().AddFlagSet(flags)

return cmd
}
Expand Down Expand Up @@ -295,7 +300,7 @@ func (options *injectOptions) fetchConfigsOrDefault() (*config.All, error) {
// overrideConfigs uses command-line overrides to update the provided configs.
// the overrideAnnotations map keeps track of which configs are overridden, by
// storing the corresponding annotations and values.
func (options *injectOptions) overrideConfigs(configs *config.All, overrideAnnotations map[string]string) {
func (options *proxyConfigOptions) overrideConfigs(configs *config.All, overrideAnnotations map[string]string) {
if options.linkerdVersion != "" {
configs.Global.Version = options.linkerdVersion
}
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/inject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func testUninjectAndInject(t *testing.T, tc testCase) {
}

func testInstallConfig() *pb.All {
_, c, err := testInstallOptions().validateAndBuild()
_, c, err := testInstallOptions().validateAndBuild(nil)
if err != nil {
log.Fatalf("test install options must be valid: %s", err)
}
Expand Down
128 changes: 71 additions & 57 deletions cli/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,32 +201,61 @@ func newInstallIdentityOptionsWithDefaults() *installIdentityOptions {

func newCmdInstall() *cobra.Command {
options := newInstallOptionsWithDefaults()
flags := options.flagSet(pflag.ExitOnError)

// The base flags are recorded separately s that they can be serialized into
// the configuration in validateAndBuild.
flags := options.recordableFlagSet(pflag.ExitOnError)

cmd := &cobra.Command{
Use: "install [flags]",
Short: "Output Kubernetes configs to install Linkerd",
Long: "Output Kubernetes configs to install Linkerd.",
RunE: func(cmd *cobra.Command, args []string) error {
options.recordFlags(flags)
if !options.ignoreCluster {
exitIfClusterExists()
}

values, configs, err := options.validateAndBuild()
values, configs, err := options.validateAndBuild(flags)
if err != nil {
return err
}

return values.render(os.Stdout, configs)
},
}

cmd.PersistentFlags().AddFlagSet(flags)

// Issuer flags are currently only supported on the initial install.
cmd.PersistentFlags().AddFlagSet(options.issuerFlagSet(pflag.ExitOnError))
// Some flags are not available during upgrade, etc.
cmd.PersistentFlags().AddFlagSet(options.installOnlyFlagSet(pflag.ExitOnError))

return cmd
}

func (options *installOptions) flagSet(e pflag.ErrorHandling) *pflag.FlagSet {
func (options *installOptions) validateAndBuild(flags *pflag.FlagSet) (*installValues, *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(identityValues.toIdentityContext())

values, err := options.buildValuesWithoutIdentity(configs)
if err != nil {
return nil, nil, err
}
values.Identity = identityValues

return values, configs, nil
}

// recordableFlagSet returns flags usable during install or upgrade.
func (options *installOptions) recordableFlagSet(e pflag.ErrorHandling) *pflag.FlagSet {
flags := pflag.NewFlagSet("install", e)

flags.AddFlagSet(options.proxyConfigOptions.flagSet(e))
Expand Down Expand Up @@ -259,12 +288,18 @@ func (options *installOptions) flagSet(e pflag.ErrorHandling) *pflag.FlagSet {
&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",
)

return flags
}

func (options *installOptions) issuerFlagSet(e pflag.ErrorHandling) *pflag.FlagSet {
flags := pflag.NewFlagSet("issuer", e)
// installOnlyFlagSet includes flags that are only accessible at install-time
// and not at upgrade-time.
func (options *installOptions) installOnlyFlagSet(e pflag.ErrorHandling) *pflag.FlagSet {
flags := options.recordableFlagSet(e)

flags.StringVar(
&options.identityOptions.trustDomain, "identity-trust-domain", options.identityOptions.trustDomain,
Expand All @@ -282,13 +317,10 @@ func (options *installOptions) issuerFlagSet(e pflag.ErrorHandling) *pflag.FlagS
&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.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.DurationVar(
&options.identityOptions.issuanceLifetime, "identity-issuance-lifetime", options.identityOptions.issuanceLifetime,
"The amount of time for which the Identity issuer should certify identity",

flags.BoolVar(
&options.ignoreCluster, "ignore-cluster", options.ignoreCluster,
"Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)",
)

return flags
Expand Down Expand Up @@ -331,26 +363,6 @@ func (options *installOptions) validate() error {
return errors.New("--proxy-log-level must not be empty")
}

if !options.ignoreCluster {
exists, err := linkerdConfigAlreadyExistsInCluster()
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to connect to a Kubernetes cluster to check for configuration. If this expected, use the --ignore-cluster flag.")
os.Exit(1)
}
if exists {
fmt.Fprintln(os.Stderr, "You are already running a control plane. If you would like to ignore its configuration, use the --ignore-cluster flag.")
os.Exit(1)
}
}

return nil
}

func (options *installOptions) validateAndBuild() (*installValues, *pb.All, error) {
if err := options.validate(); err != nil {
return nil, nil, err
}

if options.highAvailability {
if options.controllerReplicas == defaultControllerReplicas {
options.controllerReplicas = defaultHAControllerReplicas
Expand All @@ -366,16 +378,14 @@ func (options *installOptions) validateAndBuild() (*installValues, *pb.All, erro
}

options.identityOptions.replicas = options.controllerReplicas
identityValues, err := options.identityOptions.validateAndBuild()
if err != nil {
return nil, nil, err
}

configs := options.configs(identityValues.toIdentityContext())
return nil
}

func (options *installOptions) buildValuesWithoutIdentity(configs *pb.All) (*installValues, error) {
globalJSON, proxyJSON, installJSON, err := config.ToJSON(configs)
if err != nil {
return nil, nil, err
return nil, err
}

values := &installValues{
Expand Down Expand Up @@ -410,8 +420,6 @@ func (options *installOptions) validateAndBuild() (*installValues, *pb.All, erro
Proxy: proxyJSON,
Install: installJSON,
},

Identity: identityValues,
}

if options.highAvailability {
Expand All @@ -438,7 +446,7 @@ func (options *installOptions) validateAndBuild() (*installValues, *pb.All, erro
}
}

return values, configs, nil
return values, nil
}

func toPromLogLevel(level string) string {
Expand Down Expand Up @@ -622,42 +630,48 @@ func (options *installOptions) proxyConfig() *pb.Proxy {
}
}

// linkerdConfigAlreadyExistsInCluster checks the kubernetes API to determine
// whether a config exists.
// exitIfClusterExists checks the kubernetes API to determine
// whether a config exists and exits if it does exist or if an error is
// encountered.
//
// This bypasses the public API so that public API errors cannot cause us to
// misdiagnose a controller error to indicate that no control plane exists.
//
// If we cannot determine whether the configuration exists, an error is returned.
func linkerdConfigAlreadyExistsInCluster() (bool, error) {
api, err := k8s.NewAPI(kubeconfigPath, kubeContext)
func exitIfClusterExists() {
kubeConfig, err := k8s.GetConfig(kubeconfigPath, kubeContext)
if err != nil {
return false, err
fmt.Fprintln(os.Stderr, "Unable to build a Kubernetes client to check for configuration. If this expected, use the --ignore-cluster flag.")
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}

k, err := kubernetes.NewForConfig(api.Config)
k, err := kubernetes.NewForConfig(kubeConfig)
if err != nil {
return false, err
fmt.Fprintln(os.Stderr, "Unable to build a Kubernetes client to check for configuration. If this expected, use the --ignore-cluster flag.")
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}

c := k.CoreV1().ConfigMaps(controlPlaneNamespace)
if _, err = c.Get(k8s.ConfigConfigMapName, metav1.GetOptions{}); err != nil {
if kerrors.IsNotFound(err) {
return false, nil
return
}

return false, err
fmt.Fprintln(os.Stderr, "Unable to build a Kubernetes client to check for configuration. If this expected, use the --ignore-cluster flag.")
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}

return true, nil
fmt.Fprintln(os.Stderr, "Linkerd has already been installed on your cluster in the linkerd namespace. Please run upgrade if you'd like to update this installation. Otherwise, use the --ignore-cluster flag.")
os.Exit(1)
}

func (idopts *installIdentityOptions) validate() error {
if idopts == nil {
return nil
}

if idopts.trustDomain == "" {
if idopts.trustDomain != "" {
if errs := validation.IsDNS1123Subdomain(idopts.trustDomain); len(errs) > 0 {
return fmt.Errorf("invalid trust domain '%s': %s", idopts.trustDomain, errs[0])
}
Expand Down
10 changes: 5 additions & 5 deletions cli/cmd/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func TestRender(t *testing.T) {
defaultOptions := testInstallOptions()
defaultValues, defaultConfig, err := defaultOptions.validateAndBuild()
defaultValues, defaultConfig, err := defaultOptions.validateAndBuild(nil)
if err != nil {
t.Fatalf("Unexpected error validating options: %v", err)
}
Expand Down Expand Up @@ -53,7 +53,7 @@ func TestRender(t *testing.T) {
haOptions := testInstallOptions()
haOptions.recordedFlags = []*config.Install_Flag{{Name: "ha", Value: "true"}}
haOptions.highAvailability = true
haValues, haConfig, _ := haOptions.validateAndBuild()
haValues, haConfig, _ := haOptions.validateAndBuild(nil)

haWithOverridesOptions := testInstallOptions()
haWithOverridesOptions.recordedFlags = []*config.Install_Flag{
Expand All @@ -66,12 +66,12 @@ func TestRender(t *testing.T) {
haWithOverridesOptions.controllerReplicas = 2
haWithOverridesOptions.proxyCPURequest = "400m"
haWithOverridesOptions.proxyMemoryRequest = "300Mi"
haWithOverridesValues, haWithOverridesConfig, _ := haWithOverridesOptions.validateAndBuild()
haWithOverridesValues, haWithOverridesConfig, _ := haWithOverridesOptions.validateAndBuild(nil)

noInitContainerOptions := testInstallOptions()
noInitContainerOptions.recordedFlags = []*config.Install_Flag{{Name: "linkerd-cni-enabled", Value: "true"}}
noInitContainerOptions.noInitContainer = true
noInitContainerValues, noInitContainerConfig, _ := noInitContainerOptions.validateAndBuild()
noInitContainerValues, noInitContainerConfig, _ := noInitContainerOptions.validateAndBuild(nil)

noInitContainerWithProxyAutoInjectOptions := testInstallOptions()
noInitContainerWithProxyAutoInjectOptions.recordedFlags = []*config.Install_Flag{
Expand All @@ -80,7 +80,7 @@ func TestRender(t *testing.T) {
}
noInitContainerWithProxyAutoInjectOptions.noInitContainer = true
noInitContainerWithProxyAutoInjectOptions.proxyAutoInject = true
noInitContainerWithProxyAutoInjectValues, noInitContainerWithProxyAutoInjectConfig, _ := noInitContainerWithProxyAutoInjectOptions.validateAndBuild()
noInitContainerWithProxyAutoInjectValues, noInitContainerWithProxyAutoInjectConfig, _ := noInitContainerWithProxyAutoInjectOptions.validateAndBuild(nil)

testCases := []struct {
values *installValues
Expand Down
6 changes: 1 addition & 5 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func init() {
RootCmd.AddCommand(newCmdTap())
RootCmd.AddCommand(newCmdTop())
RootCmd.AddCommand(newCmdUninject())
RootCmd.AddCommand(newCmdUpgrade())
RootCmd.AddCommand(newCmdVersion())
}

Expand Down Expand Up @@ -300,10 +301,5 @@ func (options *proxyConfigOptions) flagSet(e pflag.ErrorHandling) *pflag.FlagSet
flags.MarkDeprecated("proxy-memory", "use --proxy-memory-request instead")
flags.MarkDeprecated("proxy-cpu", "use --proxy-cpu-request instead")

flags.BoolVar(
&options.ignoreCluster, "ignore-cluster", options.ignoreCluster,
"Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)",
)

return flags
}
Loading

0 comments on commit 96cd003

Please sign in to comment.