From 6d90ec1c774be13c3039d562cab1c582e13825e9 Mon Sep 17 00:00:00 2001 From: Yaron Schneider Date: Wed, 6 Jan 2021 11:36:20 -0800 Subject: [PATCH] Add dapr upgrade command (#566) * add dapr upgrade command * make flags required and update README * linter * remove hard-coded namespace * add unit tests * remove default runtime-version for upgrade * seperate ha from mtls * allow upgrading helm installations --- README.md | 14 ++- cmd/upgrade.go | 46 +++++++++ go.mod | 1 + pkg/kubernetes/upgrade.go | 166 +++++++++++++++++++++++++++++++++ pkg/kubernetes/upgrade_test.go | 42 +++++++++ 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 cmd/upgrade.go create mode 100644 pkg/kubernetes/upgrade.go create mode 100644 pkg/kubernetes/upgrade_test.go diff --git a/README.md b/README.md index 78ed7f39..26648c4f 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ $ dapr uninstall --network dapr-network The init command will install Dapr to a Kubernetes cluster. For more advanced use cases, use our [Helm Chart](https://github.com/dapr/dapr/tree/master/charts/dapr). -*Note: The default namespace is dapr-system* +*Note: The default namespace is dapr-system. The installation will appear under the name `dapr` for Helm* ``` $ dapr init -k @@ -211,6 +211,18 @@ To remove Dapr from your Kubernetes cluster, use the `uninstall` command with `- $ dapr uninstall -k ``` +### Upgrade Dapr on Kubernetes + +To perform a zero downtime upgrade of the Dapr control plane: + +``` +$ dapr upgrade -k --runtime-version=1.0.0-rc.2 +``` + +The example above shows how to upgrade from your current version to version `1.0.0-rc.2`. + +*Note: do not use the `dapr upgrade` command if you're upgrading from 0.x versions of Dapr* + ### Launch Dapr and your app The Dapr CLI lets you debug easily by launching both Dapr and your app. diff --git a/cmd/upgrade.go b/cmd/upgrade.go new file mode 100644 index 00000000..4ce20abe --- /dev/null +++ b/cmd/upgrade.go @@ -0,0 +1,46 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package cmd + +import ( + "os" + + "github.com/dapr/cli/pkg/kubernetes" + "github.com/dapr/cli/pkg/print" + "github.com/spf13/cobra" +) + +var UpgradeCmd = &cobra.Command{ + Use: "upgrade", + Short: "Upgrades a Dapr control plane installation in a cluster. Supported platforms: Kubernetes", + Example: ` +# Upgrade Dapr in Kubernetes +dapr upgrade -k + +# See more at: https://docs.dapr.io/getting-started/ +`, + Run: func(cmd *cobra.Command, args []string) { + err := kubernetes.Upgrade(kubernetes.UpgradeConfig{ + RuntimeVersion: runtimeVersion, + }) + if err != nil { + print.FailureStatusEvent(os.Stdout, "Failed to upgrade Dapr: %s", err) + return + } + print.SuccessStatusEvent(os.Stdout, "Dapr control plane successfully upgraded to version %s. Make sure your deployments are restarted to pick up the latest sidecar version.", runtimeVersion) + }, +} + +func init() { + UpgradeCmd.Flags().BoolVarP(&kubernetesMode, "kubernetes", "k", false, "Upgrade Dapr in a Kubernetes cluster") + UpgradeCmd.Flags().StringVarP(&runtimeVersion, "runtime-version", "", "", "The version of the Dapr runtime to upgrade to, for example: 1.0.0") + UpgradeCmd.Flags().BoolP("help", "h", false, "Print this help message") + + UpgradeCmd.MarkFlagRequired("runtime-version") + UpgradeCmd.MarkFlagRequired("kubernetes") + + RootCmd.AddCommand(UpgradeCmd) +} diff --git a/go.mod b/go.mod index 7414bb70..f160b934 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( gopkg.in/yaml.v2 v2.3.0 helm.sh/helm/v3 v3.4.0 k8s.io/api v0.20.0 + k8s.io/apiextensions-apiserver v0.20.0 k8s.io/apimachinery v0.20.0 k8s.io/cli-runtime v0.20.0 k8s.io/client-go v0.20.0 diff --git a/pkg/kubernetes/upgrade.go b/pkg/kubernetes/upgrade.go new file mode 100644 index 00000000..77d551fb --- /dev/null +++ b/pkg/kubernetes/upgrade.go @@ -0,0 +1,166 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package kubernetes + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/dapr/cli/pkg/print" + "github.com/dapr/cli/utils" + helm "helm.sh/helm/v3/pkg/action" + "k8s.io/helm/pkg/strvals" +) + +const operatorName = "dapr-operator" + +var crds = []string{ + "components", + "configuration", + "subscription", +} + +type UpgradeConfig struct { + RuntimeVersion string +} + +func Upgrade(conf UpgradeConfig) error { + sc, err := NewStatusClient() + if err != nil { + return err + } + + status, err := sc.Status() + if err != nil { + return err + } + + if len(status) == 0 { + return errors.New("dapr is not installed in your cluster") + } + + var daprVersion string + for _, s := range status { + if s.Name == operatorName { + daprVersion = s.Version + } + } + print.InfoStatusEvent(os.Stdout, "Dapr control plane version %s detected in namespace %s", daprVersion, status[0].Namespace) + + helmConf, err := helmConfig(status[0].Namespace) + if err != nil { + return err + } + + daprChart, err := daprChart(conf.RuntimeVersion, helmConf) + if err != nil { + return err + } + + upgradeClient := helm.NewUpgrade(helmConf) + upgradeClient.ResetValues = true + upgradeClient.Namespace = status[0].Namespace + upgradeClient.CleanupOnFail = true + upgradeClient.Wait = true + + print.InfoStatusEvent(os.Stdout, "Starting upgrade...") + + mtls, err := IsMTLSEnabled() + if err != nil { + return err + } + + var vals map[string]interface{} + var ca []byte + var issuerCert []byte + var issuerKey []byte + + if mtls { + secret, sErr := getTrustChainSecret() + if sErr != nil { + return sErr + } + + ca = secret.Data["ca.crt"] + issuerCert = secret.Data["issuer.crt"] + issuerKey = secret.Data["issuer.key"] + } + + ha := highAvailabilityEnabled(status) + vals, err = upgradeChartValues(string(ca), string(issuerCert), string(issuerKey), ha) + if err != nil { + return err + } + + err = applyCRDs(fmt.Sprintf("v%s", conf.RuntimeVersion)) + if err != nil { + return err + } + + listClient := helm.NewList(helmConf) + releases, err := listClient.Run() + if err != nil { + return err + } + + var chart string + for _, r := range releases { + if r.Chart != nil && strings.Contains(r.Chart.Name(), "dapr") { + chart = r.Name + break + } + } + + if _, err = upgradeClient.Run(chart, daprChart, vals); err != nil { + return err + } + return nil +} + +func highAvailabilityEnabled(status []StatusOutput) bool { + for _, s := range status { + if s.Replicas > 1 { + return true + } + } + return false +} + +func applyCRDs(version string) error { + for _, crd := range crds { + url := fmt.Sprintf("https://raw.githubusercontent.com/dapr/dapr/%s/charts/dapr/crds/%s.yaml", version, crd) + _, err := utils.RunCmdAndWait("kubectl", "apply", "-f", url) + if err != nil { + return err + } + } + return nil +} + +func upgradeChartValues(ca, issuerCert, issuerKey string, haMode bool) (map[string]interface{}, error) { + chartVals := map[string]interface{}{} + globalVals := []string{} + + if ca != "" && issuerCert != "" && issuerKey != "" { + globalVals = append(globalVals, fmt.Sprintf("dapr_sentry.tls.root.certPEM=%s", ca), + fmt.Sprintf("dapr_sentry.tls.issuer.certPEM=%s", issuerCert), + fmt.Sprintf("dapr_sentry.tls.issuer.keyPEM=%s", issuerKey), + ) + } + + if haMode { + globalVals = append(globalVals, "global.ha.enabled=true") + } + + for _, v := range globalVals { + if err := strvals.ParseInto(v, chartVals); err != nil { + return nil, err + } + } + return chartVals, nil +} diff --git a/pkg/kubernetes/upgrade_test.go b/pkg/kubernetes/upgrade_test.go new file mode 100644 index 00000000..fd62c1b7 --- /dev/null +++ b/pkg/kubernetes/upgrade_test.go @@ -0,0 +1,42 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHAMode(t *testing.T) { + t.Run("ha mode", func(t *testing.T) { + s := []StatusOutput{ + { + Replicas: 3, + }, + } + + r := highAvailabilityEnabled(s) + assert.True(t, r) + }) + + t.Run("non-ha mode", func(t *testing.T) { + s := []StatusOutput{ + { + Replicas: 1, + }, + } + + r := highAvailabilityEnabled(s) + assert.False(t, r) + }) +} + +func TestMTLSChartValues(t *testing.T) { + val, err := upgradeChartValues("1", "2", "3", true) + assert.NoError(t, err) + assert.Len(t, val, 2) +}