Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions internal/drivers/api_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@ func (d *APIGatewayDriver) HealthCheck(ctx context.Context, ref interfaces.Resou
if err != nil {
return &interfaces.HealthResult{Healthy: false, Message: err.Error()}, nil
}
healthy := app.ActiveDeployment != nil && app.ActiveDeployment.Phase == godo.DeploymentPhase_Active
return &interfaces.HealthResult{Healthy: healthy}, nil
return appHealthResult(app), nil
}

func (d *APIGatewayDriver) Scale(_ context.Context, _ interfaces.ResourceRef, _ int) (*interfaces.ResourceOutput, error) {
Expand Down
87 changes: 87 additions & 0 deletions internal/drivers/api_gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package drivers_test
import (
"context"
"fmt"
"strings"
"testing"

"github.com/GoCodeAlone/workflow-plugin-digitalocean/internal/drivers"
Expand Down Expand Up @@ -208,3 +209,89 @@ func TestAPIGatewayDriver_HealthCheck_Unhealthy(t *testing.T) {
t.Errorf("expected unhealthy when no active deployment")
}
}

// ── APIGateway HealthCheck deployment-phase tests ────────────────────────────

func gwAppWithPhases(active, inProgress, pending *godo.DeploymentPhase) *godo.App {
app := &godo.App{ID: "app-gw-999", Spec: &godo.AppSpec{Name: "phased-gw"}}
if active != nil {
app.ActiveDeployment = &godo.Deployment{Phase: *active}
}
if inProgress != nil {
app.InProgressDeployment = &godo.Deployment{Phase: *inProgress}
}
if pending != nil {
app.PendingDeployment = &godo.Deployment{Phase: *pending}
}
return app
}

func gwPhasePtr(p godo.DeploymentPhase) *godo.DeploymentPhase { return &p }

func TestAPIGatewayDriver_HealthCheck_InProgress_Building(t *testing.T) {
d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{
app: gwAppWithPhases(nil, gwPhasePtr(godo.DeploymentPhase_Building), nil),
}, "nyc3")
result, err := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Healthy {
t.Error("expected Healthy=false while BUILDING")
}
if !strings.Contains(result.Message, "in progress") {
t.Errorf("message should contain 'in progress', got: %q", result.Message)
}
}

func TestAPIGatewayDriver_HealthCheck_InProgress_Deploying(t *testing.T) {
d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{
app: gwAppWithPhases(nil, gwPhasePtr(godo.DeploymentPhase_Deploying), nil),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"})
if result.Healthy {
t.Error("expected Healthy=false while DEPLOYING")
}
if !strings.Contains(result.Message, "in progress") {
t.Errorf("message should contain 'in progress', got: %q", result.Message)
}
}

func TestAPIGatewayDriver_HealthCheck_InProgress_Failed(t *testing.T) {
d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{
app: gwAppWithPhases(nil, gwPhasePtr(godo.DeploymentPhase_Error), nil),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"})
if result.Healthy {
t.Error("expected Healthy=false for ERROR phase")
}
if !strings.Contains(result.Message, "failed") {
t.Errorf("message should contain 'failed', got: %q", result.Message)
}
}

func TestAPIGatewayDriver_HealthCheck_PendingDeployment(t *testing.T) {
d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{
app: gwAppWithPhases(nil, nil, gwPhasePtr(godo.DeploymentPhase_PendingBuild)),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"})
if result.Healthy {
t.Error("expected Healthy=false with only a pending deployment")
}
if !strings.Contains(result.Message, "queued") {
t.Errorf("message should contain 'queued', got: %q", result.Message)
}
}

func TestAPIGatewayDriver_HealthCheck_NoDeployment(t *testing.T) {
d := drivers.NewAPIGatewayDriverWithClient(&mockAPIGatewayClient{
app: gwAppWithPhases(nil, nil, nil),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-gw", ProviderID: "app-gw-999"})
if result.Healthy {
t.Error("expected Healthy=false with no deployments")
}
if !strings.Contains(result.Message, "no deployment") {
t.Errorf("message should contain 'no deployment', got: %q", result.Message)
}
}
48 changes: 43 additions & 5 deletions internal/drivers/app_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,50 @@ func (d *AppPlatformDriver) HealthCheck(ctx context.Context, ref interfaces.Reso
if err != nil {
return &interfaces.HealthResult{Healthy: false, Message: err.Error()}, nil
}
healthy := app.ActiveDeployment != nil && app.ActiveDeployment.Phase == godo.DeploymentPhase_Active
msg := ""
if !healthy {
msg = fmt.Sprintf("phase: %v", app.ActiveDeployment)
return appHealthResult(app), nil
}

// appHealthResult evaluates all three DO deployment slots in priority order and
// returns an accurate HealthResult:
//
// - ActiveDeployment ACTIVE → Healthy=true
// - InProgressDeployment (building) → Healthy=false, "deployment in progress: <phase>"
// - InProgressDeployment (failed) → Healthy=false, "deployment failed: <phase>"
// - PendingDeployment → Healthy=false, "deployment queued"
// - none of the above → Healthy=false, "no deployment found"
func appHealthResult(app *godo.App) *interfaces.HealthResult {
// 1. Active and healthy.
if app.ActiveDeployment != nil && app.ActiveDeployment.Phase == godo.DeploymentPhase_Active {
return &interfaces.HealthResult{Healthy: true}
}
return &interfaces.HealthResult{Healthy: healthy, Message: msg}, nil

// 2. Deployment currently in progress — inspect its phase.
if dep := app.InProgressDeployment; dep != nil {
switch dep.Phase {
case godo.DeploymentPhase_PendingBuild,
godo.DeploymentPhase_Building,
godo.DeploymentPhase_PendingDeploy,
godo.DeploymentPhase_Deploying:
return &interfaces.HealthResult{
Healthy: false,
Message: fmt.Sprintf("deployment in progress: %s", dep.Phase),
}
default:
// ERROR, CANCELED, SUPERSEDED, etc.
return &interfaces.HealthResult{
Healthy: false,
Message: fmt.Sprintf("deployment failed: %s", dep.Phase),
}
}
}

// 3. Deployment queued but not yet started.
if app.PendingDeployment != nil {
return &interfaces.HealthResult{Healthy: false, Message: "deployment queued"}
}

// 4. No deployment at all (first-deploy not yet kicked off, or app never deployed).
return &interfaces.HealthResult{Healthy: false, Message: "no deployment found"}
}

func (d *AppPlatformDriver) Scale(ctx context.Context, ref interfaces.ResourceRef, replicas int) (*interfaces.ResourceOutput, error) {
Expand Down
100 changes: 100 additions & 0 deletions internal/drivers/app_platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"testing"

"github.com/GoCodeAlone/workflow-plugin-digitalocean/internal/drivers"
Expand Down Expand Up @@ -334,6 +335,105 @@ func TestAppPlatformDriver_HealthCheck_Unhealthy(t *testing.T) {
}
}

// ── HealthCheck deployment-phase tests ───────────────────────────────────────

func appWithPhases(active, inProgress, pending *godo.DeploymentPhase) *godo.App {
app := &godo.App{ID: "app-999", Spec: &godo.AppSpec{Name: "phased-app"}}
if active != nil {
app.ActiveDeployment = &godo.Deployment{Phase: *active}
}
if inProgress != nil {
app.InProgressDeployment = &godo.Deployment{Phase: *inProgress}
}
if pending != nil {
app.PendingDeployment = &godo.Deployment{Phase: *pending}
}
return app
}

func phasePtr(p godo.DeploymentPhase) *godo.DeploymentPhase { return &p }

func TestAppPlatformDriver_HealthCheck_Active(t *testing.T) {
d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{
app: appWithPhases(phasePtr(godo.DeploymentPhase_Active), nil, nil),
}, "nyc3")
result, err := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Healthy {
t.Errorf("expected Healthy=true for ACTIVE phase, got message: %q", result.Message)
}
}

func TestAppPlatformDriver_HealthCheck_InProgress_Building(t *testing.T) {
d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{
app: appWithPhases(nil, phasePtr(godo.DeploymentPhase_Building), nil),
}, "nyc3")
result, err := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Healthy {
t.Error("expected Healthy=false while BUILDING")
}
if !strings.Contains(result.Message, "in progress") {
t.Errorf("message should contain 'in progress', got: %q", result.Message)
}
}

func TestAppPlatformDriver_HealthCheck_InProgress_Deploying(t *testing.T) {
d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{
app: appWithPhases(nil, phasePtr(godo.DeploymentPhase_Deploying), nil),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"})
if result.Healthy {
t.Error("expected Healthy=false while DEPLOYING")
}
if !strings.Contains(result.Message, "in progress") {
t.Errorf("message should contain 'in progress', got: %q", result.Message)
}
}

func TestAppPlatformDriver_HealthCheck_InProgress_Failed(t *testing.T) {
d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{
app: appWithPhases(nil, phasePtr(godo.DeploymentPhase_Error), nil),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"})
if result.Healthy {
t.Error("expected Healthy=false for ERROR phase")
}
if !strings.Contains(result.Message, "failed") {
t.Errorf("message should contain 'failed', got: %q", result.Message)
}
}

func TestAppPlatformDriver_HealthCheck_PendingDeployment(t *testing.T) {
d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{
app: appWithPhases(nil, nil, phasePtr(godo.DeploymentPhase_PendingBuild)),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"})
if result.Healthy {
t.Error("expected Healthy=false with only a pending deployment")
}
if !strings.Contains(result.Message, "queued") {
t.Errorf("message should contain 'queued', got: %q", result.Message)
}
}

func TestAppPlatformDriver_HealthCheck_NoDeployment(t *testing.T) {
d := drivers.NewAppPlatformDriverWithClient(&mockAppClient{
app: appWithPhases(nil, nil, nil),
}, "nyc3")
result, _ := d.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "phased-app", ProviderID: "app-999"})
if result.Healthy {
t.Error("expected Healthy=false with no deployments")
}
if !strings.Contains(result.Message, "no deployment") {
t.Errorf("message should contain 'no deployment', got: %q", result.Message)
}
}

// ── ParseImageRef unit tests ──────────────────────────────────────────────────

func TestParseImageRef_DOCR(t *testing.T) {
Expand Down
Loading