|
|
@@ -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)
|
|
|
+}
|