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
5 changes: 5 additions & 0 deletions internal/drivers/app_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type AppPlatformClient interface {
Get(ctx context.Context, appID string) (*godo.App, *godo.Response, error)
List(ctx context.Context, opts *godo.ListOptions) ([]*godo.App, *godo.Response, error)
Update(ctx context.Context, appID string, req *godo.AppUpdateRequest) (*godo.App, *godo.Response, error)
CreateDeployment(ctx context.Context, appID string, req ...*godo.DeploymentCreateRequest) (*godo.Deployment, *godo.Response, error)
Delete(ctx context.Context, appID string) (*godo.Response, error)
}

Expand Down Expand Up @@ -140,6 +141,10 @@ func (d *AppPlatformDriver) Update(ctx context.Context, ref interfaces.ResourceR
if err != nil {
return nil, fmt.Errorf("app platform update %q: %w", ref.Name, WrapGodoError(err))
}
// Trigger a new deployment — Update only changes the spec; DO does not auto-deploy.
if _, _, err := d.client.CreateDeployment(ctx, ref.ProviderID, &godo.DeploymentCreateRequest{ForceBuild: true}); err != nil {
return nil, fmt.Errorf("app platform create deployment %q: %w", ref.Name, WrapGodoError(err))
}
return appOutput(app), nil
}

Expand Down
92 changes: 86 additions & 6 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"
"net/http"
"strings"
"testing"

Expand All @@ -12,14 +13,26 @@ import (
"github.com/digitalocean/godo"
)

// makeGodoErr builds a *godo.ErrorResponse with the given HTTP status code,
// matching what WrapGodoError inspects.
func makeGodoErr(statusCode int) error {
return &godo.ErrorResponse{
Response: &http.Response{StatusCode: statusCode},
Message: http.StatusText(statusCode),
}
}

// mockAppClient is a mock implementation of AppPlatformClient.
type mockAppClient struct {
app *godo.App
err error
listApps []*godo.App // returned by List
listErr error // error returned by List
lastCreateReq *godo.AppCreateRequest
lastUpdateReq *godo.AppUpdateRequest
app *godo.App
err error
listApps []*godo.App // returned by List
listErr error // error returned by List
createDeploymentErr error // error returned by CreateDeployment
createDeploymentCalled bool
lastCreateDeployReq *godo.DeploymentCreateRequest
lastCreateReq *godo.AppCreateRequest
lastUpdateReq *godo.AppUpdateRequest
}

func (m *mockAppClient) Create(_ context.Context, req *godo.AppCreateRequest) (*godo.App, *godo.Response, error) {
Expand All @@ -36,6 +49,13 @@ func (m *mockAppClient) Update(_ context.Context, _ string, req *godo.AppUpdateR
m.lastUpdateReq = req
return m.app, nil, m.err
}
func (m *mockAppClient) CreateDeployment(_ context.Context, _ string, reqs ...*godo.DeploymentCreateRequest) (*godo.Deployment, *godo.Response, error) {
m.createDeploymentCalled = true
for _, r := range reqs {
m.lastCreateDeployReq = r
}
return &godo.Deployment{ID: "dep-1"}, nil, m.createDeploymentErr
}
func (m *mockAppClient) Delete(_ context.Context, _ string) (*godo.Response, error) {
return nil, m.err
}
Expand Down Expand Up @@ -167,6 +187,66 @@ func TestAppPlatformDriver_Update_Error(t *testing.T) {
}
}

func TestAppPlatformDriver_Update_TriggersCreateDeployment(t *testing.T) {
mock := &mockAppClient{app: testApp()}
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")

_, err := d.Update(context.Background(), interfaces.ResourceRef{
Name: "my-app", ProviderID: "app-123",
}, interfaces.ResourceSpec{
Name: "my-app",
Config: map[string]any{"image": "registry.digitalocean.com/myrepo/myapp:v2"},
})
if err != nil {
t.Fatalf("Update: %v", err)
}
if !mock.createDeploymentCalled {
t.Error("expected CreateDeployment to be called after Update")
}
if mock.lastCreateDeployReq == nil || !mock.lastCreateDeployReq.ForceBuild {
t.Error("expected CreateDeployment called with ForceBuild=true")
}
}

func TestAppPlatformDriver_Update_CreateDeploymentFails(t *testing.T) {
mock := &mockAppClient{
app: testApp(),
createDeploymentErr: fmt.Errorf("deployment quota exceeded"),
}
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")

_, err := d.Update(context.Background(), interfaces.ResourceRef{
Name: "my-app", ProviderID: "app-123",
}, interfaces.ResourceSpec{
Name: "my-app",
Config: map[string]any{"image": "registry.digitalocean.com/myrepo/myapp:v2"},
})
if err == nil {
t.Fatal("expected error when CreateDeployment fails, got nil")
}
if !strings.Contains(err.Error(), "deployment quota exceeded") {
t.Errorf("error should contain original message, got: %v", err)
}
}

func TestAppPlatformDriver_Update_CreateDeploymentSentinelPropagates(t *testing.T) {
mock := &mockAppClient{
app: testApp(),
createDeploymentErr: makeGodoErr(http.StatusTooManyRequests),
}
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")

_, err := d.Update(context.Background(), interfaces.ResourceRef{
Name: "my-app", ProviderID: "app-123",
}, interfaces.ResourceSpec{
Name: "my-app",
Config: map[string]any{"image": "registry.digitalocean.com/myrepo/myapp:v2"},
})
if !errors.Is(err, interfaces.ErrRateLimited) {
t.Errorf("expected ErrRateLimited sentinel, got: %v", err)
}
}

func TestAppPlatformDriver_Delete_Error(t *testing.T) {
mock := &mockAppClient{err: fmt.Errorf("delete failed")}
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")
Expand Down
7 changes: 7 additions & 0 deletions internal/drivers/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ func (m *deployMockClient) List(_ context.Context, _ *godo.ListOptions) ([]*godo
return apps, &godo.Response{}, nil
}

func (m *deployMockClient) CreateDeployment(_ context.Context, _ string, _ ...*godo.DeploymentCreateRequest) (*godo.Deployment, *godo.Response, error) {
if m.err != nil {
return nil, nil, m.err
}
return &godo.Deployment{ID: "dep-deploy"}, nil, nil
}

func (m *deployMockClient) Delete(_ context.Context, appID string) (*godo.Response, error) {
if m.err != nil {
return nil, m.err
Expand Down
Loading