Forráskód Böngészése

add mechanism for blue-green deployments from the CLI

Alexander Belanger 4 éve
szülő
commit
73a2be313b
2 módosított fájl, 213 hozzáadás és 0 törlés
  1. 192 0
      cli/cmd/bluegreen.go
  2. 21 0
      cli/cmd/deploy/deploy.go

+ 192 - 0
cli/cmd/bluegreen.go

@@ -0,0 +1,192 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/spf13/cobra"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	intstrutil "k8s.io/apimachinery/pkg/util/intstr"
+)
+
+var deployCmd = &cobra.Command{
+	Use: "deploy",
+}
+
+var bluegreenCmd = &cobra.Command{
+	Use:   "blue-green-switch",
+	Short: "Automatically switches the traffic of a blue-green deployment once the new application is ready.",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, bluegreenSwitch)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(deployCmd)
+	deployCmd.AddCommand(bluegreenCmd)
+
+	bluegreenCmd.PersistentFlags().StringVar(
+		&app,
+		"app",
+		"",
+		"Application in the Porter dashboard",
+	)
+
+	bluegreenCmd.MarkPersistentFlagRequired("app")
+
+	bluegreenCmd.PersistentFlags().StringVar(
+		&tag,
+		"tag",
+		"",
+		"The image tag to switch traffic to.",
+	)
+
+	bluegreenCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"",
+		"The namespace of the jobs.",
+	)
+}
+
+func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	// get the web release
+	webRelease, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, app)
+
+	if err != nil {
+		return err
+	}
+
+	// if this application is not a web chart, throw an error
+	if webRelease.Chart.Name() != "web" {
+		return fmt.Errorf("target application is not a web chart")
+	}
+
+	// TODO: check that bluegreen is enabled
+
+	// get the replicasets attached to the deployment
+	sharedConf := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = sharedConf.setSharedConfig()
+
+	if err != nil {
+		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
+	}
+
+	// if no job exists with the given revision, wait up to 30 minutes
+	timeWait := time.Now().Add(30 * time.Minute)
+	prevRefresh := time.Now()
+
+	success := false
+
+	for time.Now().Before(timeWait) {
+		// refresh the client every 10 minutes
+		if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
+			err = sharedConf.setSharedConfig()
+
+			if err != nil {
+				return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
+			}
+
+			prevRefresh = time.Now()
+		}
+
+		depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
+			context.Background(),
+			metav1.ListOptions{
+				LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
+			},
+		)
+
+		if err != nil {
+			return fmt.Errorf("could not get deployments: %s", err.Error())
+		}
+
+		foundDeployment := false
+
+		// get the deployment which matches the new image tag
+		for _, depl := range depls.Items {
+			if depl.ObjectMeta.Name == fmt.Sprintf("%s-web-%s", app, tag) {
+				foundDeployment = true
+
+				// determine if the deployment has an appropriate number of ready replicas
+				minUnavailable := *(depl.Spec.Replicas) - getMaxUnavailable(depl)
+
+				fmt.Println("min unavabile is", minUnavailable, "curr is", depl.Status.ReadyReplicas)
+
+				// if the number of ready replicas is greater than the number of min unavailable,
+				// the controller is ready for a traffic switch
+				if minUnavailable <= depl.Status.ReadyReplicas {
+					// push the deployment
+					color.New(color.FgGreen).Printf("Switching traffic for app %s\n", app)
+
+					deployAgent, err := updateGetAgent(client)
+
+					if err != nil {
+						return err
+					}
+
+					err = deployAgent.UpdateImageAndValues(map[string]interface{}{
+						"bluegreen": map[string]interface{}{
+							"enabled":        true,
+							"activeImageTag": tag,
+							"imageTags":      []string{tag},
+						},
+					})
+
+					if err != nil {
+						return err
+					} else {
+						success = true
+					}
+				}
+			}
+		}
+
+		if !foundDeployment {
+			return fmt.Errorf("target deployment not found. Did you specify the correct tag?")
+		}
+
+		if success {
+			break
+		}
+
+		// otherwise, return no error
+		time.Sleep(2 * time.Second)
+	}
+
+	if !success {
+		return fmt.Errorf("new application was not ready within 30 minutes")
+	}
+
+	return nil
+}
+
+func getMaxUnavailable(deployment appsv1.Deployment) int32 {
+	if deployment.Spec.Strategy.Type != appsv1.RollingUpdateDeploymentStrategyType || *(deployment.Spec.Replicas) == 0 {
+		return int32(0)
+	}
+
+	desired := *(deployment.Spec.Replicas)
+	maxUnavailable := deployment.Spec.Strategy.RollingUpdate.MaxUnavailable
+
+	unavailable, err := intstrutil.GetScaledValueFromIntOrPercent(intstrutil.ValueOrDefault(maxUnavailable, intstrutil.FromInt(0)), int(desired), false)
+
+	if err != nil {
+		return 0
+	}
+
+	return int32(unavailable)
+}

+ 21 - 0
cli/cmd/deploy/deploy.go

@@ -341,6 +341,27 @@ func (d *DeployAgent) UpdateImageAndValues(overrideValues map[string]interface{}
 
 	mergedValues := utils.CoalesceValues(d.release.Config, overrideValues)
 
+	// if blue-green deployments are enabled, preserve the active tag value and append the new tag to the
+	// imageTags object
+	if bgInter, ok := mergedValues["bluegreen"]; ok {
+		if bgVal, ok := bgInter.(map[string]interface{}); ok {
+			if enabledInter, ok := bgVal["enabled"]; ok {
+				if enabledVal, ok := enabledInter.(bool); ok && enabledVal {
+					// they're enabled -- read the activeTagValue and construct the new bluegreen object
+					if activeTagInter, ok := bgVal["activeImageTag"]; ok {
+						if activeTagVal, ok := activeTagInter.(string); ok {
+							mergedValues["bluegreen"] = map[string]interface{}{
+								"enabled":        true,
+								"activeImageTag": activeTagVal,
+								"imageTags":      []string{activeTagVal, d.tag},
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
 	// overwrite the tag based on a new image
 	currImageSection := mergedValues["image"].(map[string]interface{})