Repository: digitalocean/doctl
Pull request #1350 (2023-02-13 20:24:19)

App Platform: fix race on Create/Update --wait flag
This PR fixes a potential race which can occur when using the --wait flag on the App Platform create and update operations.  Previously, the wait logic would list deployments to determine the deployment to track. In some cases the wrong deployment would be selected and the --wait would block indefinitely.

APPS-6813


Patch:
diff --git a/commands/apps.go b/commands/apps.go
index 51b468bf..569c9fbb 100644
--- a/commands/apps.go
+++ b/commands/apps.go
@@ -315,7 +315,7 @@ func RunAppsCreate(c *CmdConfig) error {
 	if wait {
 		apps := c.Apps()
 		notice("App creation is in progress, waiting for app to be running")
-		err := waitForActiveDeployment(apps, app.ID, "")
+		err := waitForActiveDeployment(apps, app.ID, app.GetPendingDeployment().GetID())
 		if err != nil {
 			errs = multierror.Append(errs, fmt.Errorf("app deployment couldn't enter `running` state: %v", err))
 			if err := c.Display(displayers.Apps{app}); err != nil {
@@ -393,7 +393,7 @@ func RunAppsUpdate(c *CmdConfig) error {
 	if wait {
 		apps := c.Apps()
 		notice("App update is in progress, waiting for app to be running")
-		err := waitForActiveDeployment(apps, app.ID, "")
+		err := waitForActiveDeployment(apps, app.ID, app.GetPendingDeployment().GetID())
 		if err != nil {
 			errs = multierror.Append(errs, fmt.Errorf("app deployment couldn't enter `running` state: %v", err))
 			if err := c.Display(displayers.Apps{app}); err != nil {
@@ -490,35 +490,18 @@ func waitForActiveDeployment(apps do.AppsService, appID string, deploymentID str
 			}
 		}
 
-		if deploymentID == "" {
-			deployments, err := apps.ListDeployments(appID)
-			if err != nil {
-				return err
-			}
-
-			// We could find no deployments if both of the following are true:
-			// - The deployment isn't created in the backend synchronously with the create/update request.
-			// - This is the first ever deployment of the app.
-			// Note that if the first is ever true we will do the wrong thing here and test the status of
-			// a previous deployment, however there's no better way to correlate a deployment with the
-			// request that triggered it.
-			if len(deployments) > 0 {
-				deploymentID = deployments[0].ID
-			}
-		} else {
-			deployment, err := apps.GetDeployment(appID, deploymentID)
-			if err != nil {
-				return err
-			}
+		deployment, err := apps.GetDeployment(appID, deploymentID)
+		if err != nil {
+			return err
+		}
 
-			allSuccessful := deployment.Progress.SuccessSteps == deployment.Progress.TotalSteps
-			if allSuccessful {
-				return nil
-			}
+		allSuccessful := deployment.Progress.SuccessSteps == deployment.Progress.TotalSteps
+		if allSuccessful {
+			return nil
+		}
 
-			if deployment.Progress.ErrorSteps > 0 {
-				return fmt.Errorf("error deploying app (%s) (deployment ID: %s):\n%s", appID, deployment.ID, godo.Stringify(deployment.Progress))
-			}
+		if deployment.Progress.ErrorSteps > 0 {
+			return fmt.Errorf("error deploying app (%s) (deployment ID: %s):\n%s", appID, deployment.ID, godo.Stringify(deployment.Progress))
 		}
 		attempts++
 		time.Sleep(10 * time.Second)
diff --git a/go.mod b/go.mod
index a568a332..94f1dc0d 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/containerd/continuity v0.3.0 // indirect
 	github.com/creack/pty v1.1.11
-	github.com/digitalocean/godo v1.96.0
+	github.com/digitalocean/godo v1.97.0
 	github.com/docker/cli v20.10.17+incompatible
 	github.com/docker/docker v20.10.17+incompatible
 	github.com/docker/docker-credential-helpers v0.6.4 // indirect
diff --git a/go.sum b/go.sum
index 23dc8fea..226ee8c8 100644
--- a/go.sum
+++ b/go.sum
@@ -282,6 +282,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/digitalocean/godo v1.96.0 h1:w46AC3z9upSEjxRa4jhjwYlp3XCTHpKdTFLtPWA4rXE=
 github.com/digitalocean/godo v1.96.0/go.mod h1:NRpFznZFvhHjBoqZAaOD3khVzsJ3EibzKqFL4R60dmA=
+github.com/digitalocean/godo v1.97.0 h1:p9w1yCcWMZcxFSLPToNGXA96WfUVLXqoHti6GzVomL4=
+github.com/digitalocean/godo v1.97.0/go.mod h1:NRpFznZFvhHjBoqZAaOD3khVzsJ3EibzKqFL4R60dmA=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M=
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
index 7d30a4b8..23407fe8 100644
--- a/vendor/github.com/digitalocean/godo/CHANGELOG.md
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Change Log
 
+## [v1.97.0] - 2023-02-10
+
+- #601 - @jcodybaker - APPS-6813: update app platform - pending_deployment + timing
+- #602 - @jcodybaker - Use App Platform active deployment for GetLogs if not specified
+
 ## [v1.96.0] - 2023-01-23
 
 - #599 - @markpaulson - Adding PromoteReplicaToPrimary to client interface.
diff --git a/vendor/github.com/digitalocean/godo/apps.gen.go b/vendor/github.com/digitalocean/godo/apps.gen.go
index 7761d9af..221332c7 100644
--- a/vendor/github.com/digitalocean/godo/apps.gen.go
+++ b/vendor/github.com/digitalocean/godo/apps.gen.go
@@ -85,6 +85,7 @@ type App struct {
 	UpdatedAt               time.Time       `json:"updated_at,omitempty"`
 	ActiveDeployment        *Deployment     `json:"active_deployment,omitempty"`
 	InProgressDeployment    *Deployment     `json:"in_progress_deployment,omitempty"`
+	PendingDeployment       *Deployment     `json:"pending_deployment,omitempty"`
 	LastDeploymentCreatedAt time.Time       `json:"last_deployment_created_at,omitempty"`
 	LiveURL                 string          `json:"live_url,omitempty"`
 	Region                  *AppRegion      `json:"region,omitempty"`
@@ -94,7 +95,7 @@ type App struct {
 	Domains                 []*AppDomain    `json:"domains,omitempty"`
 	PinnedDeployment        *Deployment     `json:"pinned_deployment,omitempty"`
 	BuildConfig             *AppBuildConfig `json:"build_config,omitempty"`
-	// The id of the project for the app. This will be empty if there is a lookup failure.
+	// The id of the project for the app. This will be empty if there is a fleet (project) lookup failure.
 	ProjectID string `json:"project_id,omitempty"`
 }
 
@@ -594,6 +595,7 @@ type Deployment struct {
 	PreviousDeploymentID string                  `json:"previous_deployment_id,omitempty"`
 	CauseDetails         *DeploymentCauseDetails `json:"cause_details,omitempty"`
 	LoadBalancerID       string                  `json:"load_balancer_id,omitempty"`
+	Timing               *DeploymentTiming       `json:"timing,omitempty"`
 }
 
 // DeploymentCauseDetails struct for DeploymentCauseDetails
@@ -710,6 +712,30 @@ type DeploymentStaticSite struct {
 	Buildpacks []*Buildpack `json:"buildpacks,omitempty"`
 }
 
+// DeploymentTiming struct for DeploymentTiming
+type DeploymentTiming struct {
+	// Pending describes the time spent waiting for the build to begin. This may include delays related to build concurrency limits.
+	Pending string `json:"pending,omitempty"`
+	// BuildTotal describes total time between the start of the build and its completion.
+	BuildTotal string `json:"build_total,omitempty"`
+	// BuildBillable describes the time spent executing the build. As builds may run concurrently  this may be greater than the build total.
+	BuildBillable string `json:"build_billable,omitempty"`
+	// Components breaks down billable build time by component.
+	Components []*DeploymentTimingComponent `json:"components,omitempty"`
+	// DatabaseProvision describes the time spent creating databases.
+	DatabaseProvision string `json:"database_provision,omitempty"`
+	// Deploying is time spent starting containers and waiting for health checks to pass.
+	Deploying string `json:"deploying,omitempty"`
+}
+
+// DeploymentTimingComponent struct for DeploymentTimingComponent
+type DeploymentTimingComponent struct {
+	// Name of the component.
+	Name string `json:"name,omitempty"`
+	// BuildBillable is the billable build time for this component.
+	BuildBillable string `json:"build_billable,omitempty"`
+}
+
 // DeploymentWorker struct for DeploymentWorker
 type DeploymentWorker struct {
 	Name             string `json:"name,omitempty"`
diff --git a/vendor/github.com/digitalocean/godo/apps.go b/vendor/github.com/digitalocean/godo/apps.go
index f19ea1e5..422b48ac 100644
--- a/vendor/github.com/digitalocean/godo/apps.go
+++ b/vendor/github.com/digitalocean/godo/apps.go
@@ -323,7 +323,12 @@ func (s *AppsServiceOp) CreateDeployment(ctx context.Context, appID string, crea
 
 // GetLogs retrieves app logs.
 func (s *AppsServiceOp) GetLogs(ctx context.Context, appID, deploymentID, component string, logType AppLogType, follow bool, tailLines int) (*AppLogs, *Response, error) {
-	url := fmt.Sprintf("%s/%s/deployments/%s/logs?type=%s&follow=%t&tail_lines=%d", appsBasePath, appID, deploymentID, logType, follow, tailLines)
+	var url string
+	if deploymentID == "" {
+		url = fmt.Sprintf("%s/%s/logs?type=%s&follow=%t&tail_lines=%d", appsBasePath, appID, logType, follow, tailLines)
+	} else {
+		url = fmt.Sprintf("%s/%s/deployments/%s/logs?type=%s&follow=%t&tail_lines=%d", appsBasePath, appID, deploymentID, logType, follow, tailLines)
+	}
 	if component != "" {
 		url = fmt.Sprintf("%s&component_name=%s", url, component)
 	}
diff --git a/vendor/github.com/digitalocean/godo/apps_accessors.go b/vendor/github.com/digitalocean/godo/apps_accessors.go
index 6af35b2c..d3692a18 100644
--- a/vendor/github.com/digitalocean/godo/apps_accessors.go
+++ b/vendor/github.com/digitalocean/godo/apps_accessors.go
@@ -110,6 +110,14 @@ func (a *App) GetOwnerUUID() string {
 	return a.OwnerUUID
 }
 
+// GetPendingDeployment returns the PendingDeployment field.
+func (a *App) GetPendingDeployment() *Deployment {
+	if a == nil {
+		return nil
+	}
+	return a.PendingDeployment
+}
+
 // GetPinnedDeployment returns the PinnedDeployment field.
 func (a *App) GetPinnedDeployment() *Deployment {
 	if a == nil {
@@ -2102,6 +2110,14 @@ func (d *Deployment) GetTierSlug() string {
 	return d.TierSlug
 }
 
+// GetTiming returns the Timing field.
+func (d *Deployment) GetTiming() *DeploymentTiming {
+	if d == nil {
+		return nil
+	}
+	return d.Timing
+}
+
 // GetUpdatedAt returns the UpdatedAt field.
 func (d *Deployment) GetUpdatedAt() time.Time {
 	if d == nil {
@@ -2510,6 +2526,70 @@ func (d *DeploymentStaticSite) GetSourceCommitHash() string {
 	return d.SourceCommitHash
 }
 
+// GetBuildBillable returns the BuildBillable field.
+func (d *DeploymentTiming) GetBuildBillable() string {
+	if d == nil {
+		return ""
+	}
+	return d.BuildBillable
+}
+
+// GetBuildTotal returns the BuildTotal field.
+func (d *DeploymentTiming) GetBuildTotal() string {
+	if d == nil {
+		return ""
+	}
+	return d.BuildTotal
+}
+
+// GetComponents returns the Components field.
+func (d *DeploymentTiming) GetComponents() []*DeploymentTimingComponent {
+	if d == nil {
+		return nil
+	}
+	return d.Components
+}
+
+// GetDatabaseProvision returns the DatabaseProvision field.
+func (d *DeploymentTiming) GetDatabaseProvision() string {
+	if d == nil {
+		return ""
+	}
+	return d.DatabaseProvision
+}
+
+// GetDeploying returns the Deploying field.
+func (d *DeploymentTiming) GetDeploying() string {
+	if d == nil {
+		return ""
+	}
+	return d.Deploying
+}
+
+// GetPending returns the Pending field.
+func (d *DeploymentTiming) GetPending() string {
+	if d == nil {
+		return ""
+	}
+	return d.Pending
+}
+
+// GetBuildBillable returns the BuildBillable field.
+func (d *DeploymentTimingComponent) GetBuildBillable() string {
+	if d == nil {
+		return ""
+	}
+	return d.BuildBillable
+}
+
+// GetName returns the Name field.
+func (d *DeploymentTimingComponent) GetName() string {
+	if d == nil {
+		return ""
+	}
+	return d.Name
+}
+
 // GetBuildpacks returns the Buildpacks field.
 func (d *DeploymentWorker) GetBuildpacks() []*Buildpack {
 	if d == nil {
diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go
index 10272082..2523f661 100644
--- a/vendor/github.com/digitalocean/godo/godo.go
+++ b/vendor/github.com/digitalocean/godo/godo.go
@@ -21,7 +21,7 @@ import (
 )
 
 const (
-	libraryVersion = "1.96.0"
+	libraryVersion = "1.97.0"
 	defaultBaseURL = "https://api.digitalocean.com/"
 	userAgent      = "godo/" + libraryVersion
 	mediaType      = "application/json"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d24067e5..690048b2 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -85,7 +85,7 @@ github.com/creack/pty
 # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 ## explicit
 github.com/davecgh/go-spew/spew
-# github.com/digitalocean/godo v1.96.0
+# github.com/digitalocean/godo v1.97.0
 ## explicit; go 1.18
 github.com/digitalocean/godo
 github.com/digitalocean/godo/metrics


Test patch:
diff --git a/integration/apps_test.go b/integration/apps_test.go
index db1f2afd..c861195b 100644
--- a/integration/apps_test.go
+++ b/integration/apps_test.go
@@ -89,11 +89,12 @@ var (
 	}
 	testAppUUID = "93a37175-f520-4a12-a7ad-26e63491dbf4"
 	testApp     = &godo.App{
-		ID:               testAppUUID,
-		Spec:             &testAppSpec,
-		ActiveDeployment: testDeployment,
-		CreatedAt:        testAppTime,
-		UpdatedAt:        testAppTime,
+		ID:                testAppUUID,
+		Spec:              &testAppSpec,
+		ActiveDeployment:  testDeployment,
+		PendingDeployment: testDeployment,
+		CreatedAt:         testAppTime,
+		UpdatedAt:         testAppTime,
 	}
 	testAppInProgress = &godo.App{
 		ID:                   testAppUUID,
@@ -294,7 +295,7 @@ var _ = suite("apps/create", func(t *testing.T, when spec.G, it spec.S) {
 			)
 			output, err := cmd.CombinedOutput()
 			expect.NoError(err)
-			expectedOutput := "Notice: App creation is in progress, waiting for app to be running\n..\nNotice: App created\n" + testAppsOutput
+			expectedOutput := "Notice: App creation is in progress, waiting for app to be running\n.\nNotice: App created\n" + testAppsOutput
 			expect.Equal(expectedOutput, strings.TrimSpace(string(output)))
 		})
 	})
@@ -658,7 +659,7 @@ var _ = suite("apps/update", func(t *testing.T, when spec.G, it spec.S) {
 			)
 			output, err := cmd.CombinedOutput()
 			expect.NoError(err)
-			expectedOutput := "Notice: App update is in progress, waiting for app to be running\n..\nNotice: App updated\n" + testAppsOutput
+			expectedOutput := "Notice: App update is in progress, waiting for app to be running\n.\nNotice: App updated\n" + testAppsOutput
 			expect.Equal(expectedOutput, strings.TrimSpace(string(output)))
 		})
 	})