diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 621fc07a38335..89cf9fb697242 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -211,7 +211,7 @@ func newCmdInstall() *cobra.Command { // The base flags are recorded separately s that they can be serialized into // the configuration in validateAndBuild. - flags := options.recordableFlagSet(pflag.ExitOnError) + flags := options.recordableFlagSet() cmd := &cobra.Command{ Use: "install [flags]", @@ -234,7 +234,7 @@ func newCmdInstall() *cobra.Command { cmd.PersistentFlags().AddFlagSet(flags) // Some flags are not available during upgrade, etc. - cmd.PersistentFlags().AddFlagSet(options.installOnlyFlagSet(pflag.ExitOnError)) + cmd.PersistentFlags().AddFlagSet(options.installOnlyFlagSet()) return cmd } @@ -262,8 +262,9 @@ func (options *installOptions) validateAndBuild(flags *pflag.FlagSet) (*installV } // recordableFlagSet returns flags usable during install or upgrade. -//nolint:unparam -func (options *installOptions) recordableFlagSet(e pflag.ErrorHandling) *pflag.FlagSet { +func (options *installOptions) recordableFlagSet() *pflag.FlagSet { + e := pflag.ExitOnError + flags := pflag.NewFlagSet("install", e) flags.AddFlagSet(options.proxyConfigOptions.flagSet(e)) @@ -311,8 +312,8 @@ func (options *installOptions) recordableFlagSet(e pflag.ErrorHandling) *pflag.F // 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 := pflag.NewFlagSet("install-only", e) +func (options *installOptions) installOnlyFlagSet() *pflag.FlagSet { + flags := pflag.NewFlagSet("install-only", pflag.ExitOnError) flags.StringVar( &options.identityOptions.trustDomain, "identity-trust-domain", options.identityOptions.trustDomain, @@ -482,6 +483,7 @@ func toPromLogLevel(level string) string { } } +// TODO: are `installValues.Configs` and `configs` redundant? func (values *installValues) render(w io.Writer, configs *pb.All) error { // Render raw values and create chart config rawValues, err := yaml.Marshal(values) diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go index e55c2c7c9c88e..edffb39c17d42 100644 --- a/cli/cmd/upgrade.go +++ b/cli/cmd/upgrade.go @@ -32,7 +32,7 @@ func newUpgradeOptionsWithDefaults() *upgradeOptions { func newCmdUpgrade() *cobra.Command { options := newUpgradeOptionsWithDefaults() - flags := options.recordableFlagSet(pflag.ExitOnError) + flags := options.recordableFlagSet() cmd := &cobra.Command{ Use: "upgrade [flags]", @@ -43,22 +43,31 @@ Note that the default flag values for this command come from the Linkerd control plane. The default values displayed in the Flags section below only apply to the install command.`, RunE: func(cmd *cobra.Command, args []string) error { + if options.ignoreCluster { + panic("ignore cluster must be unset") // Programmer error. + } + // We need a Kubernetes client to fetch configs and issuer secrets. - k, err := options.newK8s() + c, err := k8s.GetConfig(kubeconfigPath, kubeContext) + if err != nil { + upgradeErrorf("Failed to get kubernetes config: %s", err) + } + + k, err := kubernetes.NewForConfig(c) if err != nil { upgradeErrorf("Failed to create a kubernetes client: %s", err) } values, configs, err := options.validateAndBuild(k, flags) if err != nil { - return err + upgradeErrorf("Failed to build upgrade configuration: %s", err) } // rendering to a buffer and printing full contents of buffer after // render is complete, to ensure that okStatus prints separately var buf bytes.Buffer if err = values.render(&buf, configs); err != nil { - upgradeErrorf("Could not render install configuration: %s", err) + upgradeErrorf("Could not render upgrade configuration: %s", err) } buf.WriteTo(os.Stdout) @@ -74,29 +83,29 @@ install command.`, } func (options *upgradeOptions) validateAndBuild(k kubernetes.Interface, flags *pflag.FlagSet) (*installValues, *pb.All, error) { + if err := options.validate(); err != nil { + return nil, nil, err + } + // We fetch the configs directly from kubernetes because we need to be able // to upgrade/reinstall the control plane when the API is not available; and // this also serves as a passive check that we have privileges to access this // control plane. configs, err := fetchConfigs(k) if err != nil { - upgradeErrorf("Could not fetch configs from kubernetes: %s", err) + return nil, nil, fmt.Errorf("could not fetch configs from kubernetes: %s", err) } // If the install config needs to be repaired--either because it did not // exist or because it is missing expected fields, repair it. - options.repairInstall(configs.Install) + repairInstall(options.generateUUID, configs.Install) // We recorded flags during a prior install. If we haven't overridden the // flag on this upgrade, reset that prior value as if it were specified now. // // This implies that the default flag values for the upgrade command come // from the control-plane, and not from the defaults specified in the FlagSet. - setOptionsFromInstall(flags, configs.GetInstall()) - - if err = options.validate(); err != nil { - return nil, nil, err - } + setFlagsFromInstall(flags, configs.GetInstall().GetFlags()) // Save off the updated set of flags into the installOptions so it gets // persisted with the upgraded config. @@ -104,6 +113,9 @@ func (options *upgradeOptions) validateAndBuild(k kubernetes.Interface, flags *p // Update the configs from the synthesized options. options.overrideConfigs(configs, map[string]string{}) + if options.proxyAutoInject { + configs.GetGlobal().AutoInjectContext = &pb.AutoInjectContext{} + } configs.GetInstall().Flags = options.recordedFlags var identity *installIdentityValues @@ -111,15 +123,15 @@ func (options *upgradeOptions) validateAndBuild(k kubernetes.Interface, flags *p if idctx.GetTrustDomain() == "" || idctx.GetTrustAnchorsPem() == "" { // If there wasn't an idctx, or if it doesn't specify the required fields, we // must be upgrading from a version that didn't support identity, so generate it anew... - identity, err = options.installOptions.identityOptions.genValues() + identity, err = options.identityOptions.genValues() if err != nil { - upgradeErrorf("Unable to generate issuer credentials.\nError: %s", err) + return nil, nil, fmt.Errorf("unable to generate issuer credentials: %s", err) } configs.GetGlobal().IdentityContext = identity.toIdentityContext() } else { identity, err = fetchIdentityValues(k, options.controllerReplicas, idctx) if err != nil { - upgradeErrorf("Unable to fetch the existing issuer credentials from Kubernetes.\nError: %s", err) + return nil, nil, fmt.Errorf("unable to fetch the existing issuer credentials from Kubernetes: %s", err) } } @@ -127,49 +139,29 @@ func (options *upgradeOptions) validateAndBuild(k kubernetes.Interface, flags *p // otherwise it will be missing from the generated configmap. values, err := options.buildValuesWithoutIdentity(configs) if err != nil { - upgradeErrorf("Could not build install configuration: %s", err) + return nil, nil, fmt.Errorf("could not build install configuration: %s", err) } values.Identity = identity return values, configs, nil } -func setOptionsFromInstall(flags *pflag.FlagSet, install *pb.Install) { - for _, i := range install.GetFlags() { +func setFlagsFromInstall(flags *pflag.FlagSet, installFlags []*pb.Install_Flag) { + for _, i := range installFlags { if f := flags.Lookup(i.GetName()); f != nil && !f.Changed { f.Value.Set(i.GetValue()) f.Changed = true } } } -func (options *upgradeOptions) overrideConfigs(configs *pb.All, overrideAnnotations map[string]string) { - options.installOptions.overrideConfigs(configs, overrideAnnotations) - - if options.proxyAutoInject { - configs.GetGlobal().AutoInjectContext = &pb.AutoInjectContext{} - } -} - -func (options *upgradeOptions) newK8s() (kubernetes.Interface, error) { - if options.ignoreCluster { - panic("ignore cluster must be unset") // Programmer error. - } - - c, err := k8s.GetConfig(kubeconfigPath, kubeContext) - if err != nil { - return nil, err - } - - return kubernetes.NewForConfig(c) -} -func (options *upgradeOptions) repairInstall(install *pb.Install) { +func repairInstall(generateUUID func() string, install *pb.Install) { if install == nil { install = &pb.Install{} } if install.GetUuid() == "" { - install.Uuid = options.generateUUID() + install.Uuid = generateUUID() } // ALWAYS update the CLI version to the most recent. diff --git a/cli/cmd/upgrade_test.go b/cli/cmd/upgrade_test.go index 2a1cab134c809..d5a51087318c1 100644 --- a/cli/cmd/upgrade_test.go +++ b/cli/cmd/upgrade_test.go @@ -3,11 +3,16 @@ package cmd import ( "bytes" "encoding/json" + "errors" + "fmt" + "reflect" "testing" + "github.com/golang/protobuf/proto" pb "github.com/linkerd/linkerd2/controller/gen/config" "github.com/linkerd/linkerd2/pkg/k8s" - "github.com/spf13/pflag" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" ) const upgradeVersion = "TEST-VERSION" @@ -19,7 +24,13 @@ func testUpgradeOptions() *upgradeOptions { } func TestRenderUpgrade(t *testing.T) { - k8sConfigs := []string{` + testCases := []struct { + k8sConfigs []string + outputfile string + err error + }{ + { + []string{` kind: ConfigMap apiVersion: v1 metadata: @@ -36,7 +47,7 @@ data: {"proxyImage":{"imageName":"gcr.io/linkerd-io/proxy","pullPolicy":"IfNotPresent"},"proxyInitImage":{"imageName":"gcr.io/linkerd-io/proxy-init","pullPolicy":"IfNotPresent"},"controlPort":{"port":4190},"ignoreInboundPorts":[],"ignoreOutboundPorts":[],"inboundPort":{"port":4143},"adminPort":{"port":4191},"outboundPort":{"port":4140},"resource":{"requestCpu":"","requestMemory":"","limitCpu":"","limitMemory":""},"proxyUid":"2102","logLevel":{"level":"warn,linkerd2_proxy=info"},"disableExternalProfiles":true} install: | {"uuid":"57af298c-58b0-43fc-8d88-3c338789bfbc","cliVersion":"edge-19.4.1","flags":[]}`, - ` + ` kind: Secret apiVersion: v1 metadata: @@ -50,30 +61,44 @@ metadata: data: crt.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJnekNDQVNtZ0F3SUJBZ0lCQVRBS0JnZ3Foa2pPUFFRREFqQXBNU2N3SlFZRFZRUURFeDVwWkdWdWRHbDAKZVM1c2FXNXJaWEprTG1Oc2RYTjBaWEl1Ykc5allXd3dIaGNOTVRrd05EQTBNak0xTXpNM1doY05NakF3TkRBegpNak0xTXpVM1dqQXBNU2N3SlFZRFZRUURFeDVwWkdWdWRHbDBlUzVzYVc1clpYSmtMbU5zZFhOMFpYSXViRzlqCllXd3dXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVCtTYjVYNHdpNFhQMFgzckp3TXAyM1ZCZGcKRU1NVThFVStLRzhVSTJMbUM1VmpnNVJXTE9XNkJKakJtalhWaUtNK2IrMS9vS0FlT2c2RnJKazhxeUZsbzBJdwpRREFPQmdOVkhROEJBZjhFQkFNQ0FRWXdIUVlEVlIwbEJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01DCk1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0NnWUlLb1pJemowRUF3SURTQUF3UlFJaEFLVUZHM3NZT1MrK2Jha1cKWW1KWlU0NWlDZFRMdGFlbE1EU0ZpSG9DOWVCS0FpQkRXenpvKy9DWUxMbW4zM2JBRW44cFFub2dQNEZ4MDZhagorVTlLNFdsYnpBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= key.pem: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhaaEFWTnNwSlRzMWZ4YmZ4VmptTTJvMTNTOFd4U2VVdTlrNFhZK0NPY3JvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFL2ttK1YrTUl1Rno5Rjk2eWNES2R0MVFYWUJEREZQQkZQaWh2RkNOaTVndVZZNE9VVml6bAp1Z1NZd1pvMTFZaWpQbS90ZjZDZ0hqb09oYXlaUEtzaFpRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=`, + }, + "upgrade_default.golden", + nil, + }, + { + []string{}, + "", + errors.New("could not fetch configs from kubernetes: configmaps \"linkerd-config\" not found"), + }, } - options := testUpgradeOptions() - flags := options.recordableFlagSet(pflag.ExitOnError) - - clientset, _, err := k8s.NewFakeClientSets(k8sConfigs...) - if err != nil { - t.Fatalf("Error mocking k8s client: %s", err) - } - - values, configs, err := options.validateAndBuild(clientset, flags) - if err != nil { - t.Fatalf("validateAndBuild failed with %s", err) - } - - if configs.GetGlobal().GetVersion() != upgradeVersion { - t.Errorf("version not upgraded in config") - } - - var buf bytes.Buffer - if err = values.render(&buf, configs); err != nil { - t.Fatalf("could not render upgrade configuration: %s", err) + for i, tc := range testCases { + tc := tc // pin + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + options := testUpgradeOptions() + flags := options.recordableFlagSet() + + clientset, _, err := k8s.NewFakeClientSets(tc.k8sConfigs...) + if err != nil { + t.Fatalf("Error mocking k8s client: %s", err) + } + + values, configs, err := options.validateAndBuild(clientset, flags) + if !reflect.DeepEqual(err, tc.err) { + t.Fatalf("Expected \"%s\", got \"%s\"", tc.err, err) + } else if err == nil { + if configs.GetGlobal().GetVersion() != upgradeVersion { + t.Errorf("version not upgraded in config") + } + + var buf bytes.Buffer + if err = values.render(&buf, configs); err != nil { + t.Fatalf("could not render upgrade configuration: %s", err) + } + diffTestdata(t, tc.outputfile, buf.String()) + } + }) } - diffTestdata(t, "upgrade_default.golden", buf.String()) } func TestUpgradeFromOldConfig(t *testing.T) { @@ -97,9 +122,9 @@ data: `, } - options := newUpgradeOptionsWithDefaults() + options := testUpgradeOptions() options.proxyAutoInject = true - flags := options.recordableFlagSet(pflag.ExitOnError) + flags := options.recordableFlagSet() clientset, _, err := k8s.NewFakeClientSets(k8sConfigs...) if err != nil { @@ -137,3 +162,91 @@ data: t.Errorf("autoinject config not serialized") } } + +func TestFetchConfigs(t *testing.T) { + options := testInstallOptions() + _, exp, err := options.validateAndBuild(nil) + if err != nil { + t.Fatalf("Unexpected error validating options: %v", err) + } + + testCases := []struct { + k8sConfigs []string + expected *pb.All + err error + }{ + { + []string{` +kind: ConfigMap +apiVersion: v1 +metadata: + name: linkerd-config + namespace: linkerd +data: + global: | + {"linkerdNamespace":"linkerd","cniEnabled":false,"version":"dev-undefined","identityContext":{"trustDomain":"cluster.local","trustAnchorsPem":"-----BEGIN CERTIFICATE-----\nMIIBYDCCAQegAwIBAgIBATAKBggqhkjOPQQDAjAYMRYwFAYDVQQDEw1jbHVzdGVy\nLmxvY2FsMB4XDTE5MDMwMzAxNTk1MloXDTI5MDIyODAyMDM1MlowGDEWMBQGA1UE\nAxMNY2x1c3Rlci5sb2NhbDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAChpAt0\nxtgO9qbVtEtDK80N6iCL2Htyf2kIv2m5QkJ1y0TFQi5hTVe3wtspJ8YpZF0pl364\n6TiYeXB8tOOhIACjQjBAMA4GA1UdDwEB/wQEAwIBBjAdBgNVHSUEFjAUBggrBgEF\nBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNHADBE\nAiBQ/AAwF8kG8VOmRSUTPakSSa/N4mqK2HsZuhQXCmiZHwIgZEzI5DCkpU7w3SIv\nOLO4Zsk1XrGZHGsmyiEyvYF9lpY=\n-----END CERTIFICATE-----\n","issuanceLifetime":"86400s","clockSkewAllowance":"20s"},"autoInjectContext":null} + proxy: | + {"proxyImage":{"imageName":"gcr.io/linkerd-io/proxy","pullPolicy":"IfNotPresent"},"proxyInitImage":{"imageName":"gcr.io/linkerd-io/proxy-init","pullPolicy":"IfNotPresent"},"controlPort":{"port":4190},"ignoreInboundPorts":[],"ignoreOutboundPorts":[],"inboundPort":{"port":4143},"adminPort":{"port":4191},"outboundPort":{"port":4140},"resource":{"requestCpu":"","requestMemory":"","limitCpu":"","limitMemory":""},"proxyUid":"2102","logLevel":{"level":"warn,linkerd2_proxy=info"},"disableExternalProfiles":true} + install: | + {"uuid":"deaab91a-f4ab-448a-b7d1-c832a2fa0a60","cliVersion":"dev-undefined","flags":[]}`, + }, + exp, + nil, + }, + { + []string{` +kind: ConfigMap +apiVersion: v1 +metadata: + name: linkerd-config + namespace: linkerd +data: + global: | + {"linkerdNamespace":"ns","identityContext":null} + proxy: "{}" + install: "{}"`, + }, + &pb.All{Global: &pb.Global{LinkerdNamespace: "ns", IdentityContext: nil}, Proxy: &pb.Proxy{}, Install: &pb.Install{}}, + nil, + }, + { + []string{` +kind: ConfigMap +apiVersion: v1 +metadata: + name: linkerd-config + namespace: linkerd +data: + global: "{}" + proxy: "{}" + install: "{}"`, + }, + &pb.All{Global: &pb.Global{}, Proxy: &pb.Proxy{}, Install: &pb.Install{}}, + nil, + }, + { + nil, + nil, + k8sErrors.NewNotFound(schema.GroupResource{Resource: "configmaps"}, "linkerd-config"), + }, + } + + for i, tc := range testCases { + tc := tc // pin + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + clientset, _, err := k8s.NewFakeClientSets(tc.k8sConfigs...) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + configs, err := fetchConfigs(clientset) + if !reflect.DeepEqual(err, tc.err) { + t.Fatalf("Expected \"%+v\", got \"%+v\"", tc.err, err) + } + + if !proto.Equal(configs, tc.expected) { + t.Fatalf("Unexpected config:\nExpected:\n%+v\nGot:\n%+v", tc.expected, configs) + } + }) + } +}