Prechádzať zdrojové kódy

Merge pull request #2549 from porter-dev/nafees/cli-update-wait

[POR-290] Add --wait flag to `porter update` to wait for successful deployment
abelanger5 3 rokov pred
rodič
commit
e7b52b48c1
1 zmenil súbory, kde vykonal 176 pridanie a 2 odobranie
  1. 176 2
      cli/cmd/deploy.go

+ 176 - 2
cli/cmd/deploy.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 	"time"
 
@@ -18,6 +19,7 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/util/homedir"
 )
 
@@ -224,7 +226,7 @@ var updateEnvGroupCmd = &cobra.Command{
 	Aliases: []string{"eg", "envgroup", "env-groups", "envgroups"},
 	Short:   "Updates an environment group's variables, specified by the --name flag.",
 	Run: func(cmd *cobra.Command, args []string) {
-		color.New(color.FgRed).Println("need to specify an operation to continue")
+		color.New(color.FgRed).Fprintln(os.Stderr, "need to specify an operation to continue")
 	},
 }
 
@@ -268,6 +270,7 @@ var version uint
 var varType string
 var normalEnvGroupVars []string
 var secretEnvGroupVars []string
+var waitForSuccessfulDeploy bool
 
 func init() {
 	buildFlagsEnv = []string{}
@@ -372,6 +375,13 @@ func init() {
 
 	updateCmd.PersistentFlags().MarkDeprecated("force-push", "--force-push is now deprecated")
 
+	updateCmd.PersistentFlags().BoolVar(
+		&waitForSuccessfulDeploy,
+		"wait",
+		false,
+		"set this to wait and be notified when a deployment is successful, otherwise time out",
+	)
+
 	updateCmd.AddCommand(updateGetEnvCmd)
 
 	updateGetEnvCmd.PersistentFlags().StringVar(
@@ -480,6 +490,14 @@ func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 		return err
 	}
 
+	if waitForSuccessfulDeploy {
+		err := checkDeploymentStatus(client)
+
+		if err != nil {
+			return err
+		}
+	}
+
 	return nil
 }
 
@@ -588,7 +606,21 @@ func updateUpgrade(_ *types.GetAuthenticatedUserResponse, client *api.Client, ar
 		return err
 	}
 
-	return updateUpgradeWithAgent(updateAgent)
+	err = updateUpgradeWithAgent(updateAgent)
+
+	if err != nil {
+		return err
+	}
+
+	if waitForSuccessfulDeploy {
+		err := checkDeploymentStatus(client)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
 func updateSetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
@@ -1029,3 +1061,145 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 
 	return nil
 }
+
+func checkDeploymentStatus(client *api.Client) error {
+	color.New(color.FgBlue).Println("waiting for deployment to be ready, this may take a few minutes and will time out if it takes longer than 30 minutes")
+
+	sharedConf := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err := sharedConf.setSharedConfig()
+
+	if err != nil {
+		return fmt.Errorf("could not retrieve kubernetes credentials: %w", err)
+	}
+
+	prevRefresh := time.Now()
+	timeWait := prevRefresh.Add(30 * time.Minute)
+	success := false
+
+	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 for app %s: %w", app, err)
+	}
+
+	if len(depls.Items) == 0 {
+		return fmt.Errorf("could not find any deployments for app %s", app)
+	}
+
+	sort.Slice(depls.Items, func(i, j int) bool {
+		return depls.Items[i].CreationTimestamp.After(depls.Items[j].CreationTimestamp.Time)
+	})
+
+	depl := depls.Items[0]
+
+	// determine if the deployment has an appropriate number of ready replicas
+	minAvailable := *(depl.Spec.Replicas) - getMaxUnavailable(depl)
+
+	var revision string
+
+	for k, v := range depl.Spec.Template.ObjectMeta.Annotations {
+		if k == "helm.sh/revision" {
+			revision = v
+			break
+		}
+	}
+
+	if revision == "" {
+		return fmt.Errorf("could not find revision for deployment")
+	}
+
+	pods, err := sharedConf.Clientset.CoreV1().Pods(namespace).List(
+		context.Background(), metav1.ListOptions{
+			LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
+		},
+	)
+
+	if err != nil {
+		return fmt.Errorf("error fetching pods for app %s: %w", app, err)
+	}
+
+	if len(pods.Items) == 0 {
+		return fmt.Errorf("could not find any pods for app %s", app)
+	}
+
+	var rsName string
+
+	for _, pod := range pods.Items {
+		if pod.ObjectMeta.Annotations["helm.sh/revision"] == revision {
+			for _, ref := range pod.OwnerReferences {
+				if ref.Kind == "ReplicaSet" {
+					rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
+						context.Background(),
+						ref.Name,
+						metav1.GetOptions{},
+					)
+
+					if err != nil {
+						return fmt.Errorf("error fetching new replicaset: %w", err)
+					}
+
+					rsName = rs.Name
+
+					break
+				}
+			}
+
+			if rsName != "" {
+				break
+			}
+		}
+	}
+
+	if rsName == "" {
+		return fmt.Errorf("could not find replicaset for app %s", app)
+	}
+
+	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()
+		}
+
+		rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
+			context.Background(),
+			rsName,
+			metav1.GetOptions{},
+		)
+
+		if err != nil {
+			return fmt.Errorf("error fetching new replicaset: %w", err)
+		}
+
+		if minAvailable <= rs.Status.ReadyReplicas {
+			success = true
+		}
+
+		if success {
+			break
+		}
+
+		time.Sleep(2 * time.Second)
+	}
+
+	if success {
+		color.New(color.FgGreen).Printf("%s has been successfully deployed on the cluster\n", app)
+	} else {
+		return fmt.Errorf("timed out waiting for deployment to be ready, please check the Porter dashboard for more information")
+	}
+
+	return nil
+}