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
31 changes: 31 additions & 0 deletions internal/drivers/app_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ package drivers

import (
"context"
"errors"
"fmt"
"strings"

"github.com/GoCodeAlone/workflow/interfaces"
"github.com/digitalocean/godo"
)

// ErrResourceNotFound is returned when a resource cannot be located by name or ID.
// It is intended to be used with errors.Is for sentinel matching.
var ErrResourceNotFound = errors.New("resource not found")

// AppPlatformClient is the godo App interface used by AppPlatformDriver (for mocking).
type AppPlatformClient interface {
Create(ctx context.Context, req *godo.AppCreateRequest) (*godo.App, *godo.Response, error)
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)
Delete(ctx context.Context, appID string) (*godo.Response, error)
}
Expand Down Expand Up @@ -70,13 +76,38 @@ func (d *AppPlatformDriver) Create(ctx context.Context, spec interfaces.Resource
}

func (d *AppPlatformDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) {
if ref.ProviderID == "" {
return d.findAppByName(ctx, ref.Name)
}
app, _, err := d.client.Get(ctx, ref.ProviderID)
if err != nil {
return nil, fmt.Errorf("app platform read %q: %w", ref.Name, err)
}
return appOutput(app), nil
}

// findAppByName iterates the paginated app list and returns the first app whose
// Spec.Name matches name. Returns ErrResourceNotFound if no match is found.
func (d *AppPlatformDriver) findAppByName(ctx context.Context, name string) (*interfaces.ResourceOutput, error) {
opts := &godo.ListOptions{Page: 1, PerPage: 200}
for {
apps, resp, err := d.client.List(ctx, opts)
if err != nil {
return nil, fmt.Errorf("app platform list: %w", err)
}
for _, app := range apps {
if app.Spec != nil && app.Spec.Name == name {
return appOutput(app), nil
}
}
if resp == nil || resp.Links == nil || resp.Links.IsLastPage() {
break
}
opts.Page++
}
return nil, fmt.Errorf("app %q: %w", name, ErrResourceNotFound)
}

func (d *AppPlatformDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) {
imgSpec, err := imageSpecFromConfig(spec.Config)
if err != nil {
Expand Down
69 changes: 69 additions & 0 deletions internal/drivers/app_platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package drivers_test

import (
"context"
"errors"
"fmt"
"testing"

Expand All @@ -14,6 +15,8 @@ import (
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
}
Expand All @@ -25,6 +28,9 @@ func (m *mockAppClient) Create(_ context.Context, req *godo.AppCreateRequest) (*
func (m *mockAppClient) Get(_ context.Context, _ string) (*godo.App, *godo.Response, error) {
return m.app, nil, m.err
}
func (m *mockAppClient) List(_ context.Context, _ *godo.ListOptions) ([]*godo.App, *godo.Response, error) {
return m.listApps, &godo.Response{}, m.listErr
}
func (m *mockAppClient) Update(_ context.Context, _ string, req *godo.AppUpdateRequest) (*godo.App, *godo.Response, error) {
m.lastUpdateReq = req
return m.app, nil, m.err
Expand Down Expand Up @@ -503,6 +509,69 @@ func TestAppPlatformDriver_Update_BuildsNestedImageSpec(t *testing.T) {
}
}

// ── Read by name ─────────────────────────────────────────────────────────────

func TestAppPlatformDriver_Read_ByName_Found(t *testing.T) {
app := &godo.App{
ID: "app-456",
LiveURL: "https://bmw.example.com",
Spec: &godo.AppSpec{Name: "bmw-app"},
ActiveDeployment: &godo.Deployment{
Phase: godo.DeploymentPhase_Active,
},
}
mock := &mockAppClient{listApps: []*godo.App{app}}
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")

out, err := d.Read(context.Background(), interfaces.ResourceRef{
Name: "bmw-app",
// ProviderID intentionally empty — should trigger name-based lookup.
})
if err != nil {
t.Fatalf("Read by name: unexpected error: %v", err)
}
if out.ProviderID != "app-456" {
t.Errorf("ProviderID = %q, want %q", out.ProviderID, "app-456")
}
if out.Name != "bmw-app" {
t.Errorf("Name = %q, want %q", out.Name, "bmw-app")
}
if out.Status != "running" {
t.Errorf("Status = %q, want %q", out.Status, "running")
}
}

func TestAppPlatformDriver_Read_ByName_NotFound(t *testing.T) {
mock := &mockAppClient{listApps: []*godo.App{}} // empty list
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")

_, err := d.Read(context.Background(), interfaces.ResourceRef{
Name: "missing-app",
})
if err == nil {
t.Fatal("expected error for unknown name, got nil")
}
if !errors.Is(err, drivers.ErrResourceNotFound) {
t.Errorf("expected ErrResourceNotFound, got: %v", err)
}
}

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

out, err := d.Read(context.Background(), interfaces.ResourceRef{
Name: "my-app",
ProviderID: "app-123",
})
if err != nil {
t.Fatalf("Read by ID: unexpected error: %v", err)
}
if out.ProviderID != "app-123" {
t.Errorf("ProviderID = %q, want %q", out.ProviderID, "app-123")
}
}

func TestAppPlatformDriver_Create_NestedMapImageSpec(t *testing.T) {
mock := &mockAppClient{app: testApp()}
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")
Expand Down
11 changes: 11 additions & 0 deletions internal/drivers/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ func (m *deployMockClient) Update(_ context.Context, appID string, req *godo.App
return app, nil, nil
}

func (m *deployMockClient) List(_ context.Context, _ *godo.ListOptions) ([]*godo.App, *godo.Response, error) {
if m.err != nil {
return nil, nil, m.err
}
apps := make([]*godo.App, 0, len(m.apps))
for _, a := range m.apps {
apps = append(apps, a)
}
return apps, &godo.Response{}, nil
}

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