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
7 changes: 5 additions & 2 deletions internal/coolify/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,11 @@ func (c *Client) Databases(ctx context.Context) ([]Resource, error) {
// across every application. This is one call for the whole instance, which is
// what makes per-cycle polling cheap.
func (c *Client) ActiveDeployments(ctx context.Context) ([]Deployment, error) {
var out []Deployment
return out, c.do(ctx, http.MethodGet, "/deployments", &out)
var out deploymentList
if err := c.do(ctx, http.MethodGet, "/deployments", &out); err != nil {
return nil, err
}
return out, nil
}

// LatestDeployment returns the most recent deployment for an application, or
Expand Down
30 changes: 30 additions & 0 deletions internal/coolify/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package coolify
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -296,3 +297,32 @@ func TestApplicationUUIDFromDeploymentURL(t *testing.T) {
t.Errorf("unexpected url shape should yield empty uuid, got %q", got)
}
}

// Coolify filters its deployment list server-side and Laravel serializes a
// filtered collection with gaps in its keys as an object, not an array.
func TestActiveDeploymentsAcceptsKeyedObject(t *testing.T) {
for name, body := range map[string]string{
"array": `[{"deployment_uuid":"dep1","status":"in_progress"},{"deployment_uuid":"dep2","status":"queued"}]`,
"object": `{"3":{"deployment_uuid":"dep1","status":"in_progress"},"7":{"deployment_uuid":"dep2","status":"queued"}}`,
"empty": `{}`,
} {
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(body))
})
got, err := c.ActiveDeployments(context.Background())
if err != nil {
t.Fatalf("%s: ActiveDeployments: %v", name, err)
}
var uuids []string
for _, d := range got {
uuids = append(uuids, d.UUID)
}
want := "[dep1 dep2]"
if name == "empty" {
want = "[]"
}
if fmt.Sprint(uuids) != want {
t.Errorf("%s: got %v, want %s", name, uuids, want)
}
}
}
39 changes: 37 additions & 2 deletions internal/coolify/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package coolify
import (
"encoding/json"
"regexp"
"sort"
"strings"
"time"
)
Expand Down Expand Up @@ -136,9 +137,43 @@ func (d Deployment) Trigger() string {

// deploymentPage is the envelope returned by the per-application deployments
// endpoint, which paginates while the instance-wide one returns a bare array.
// deploymentList decodes Coolify's deployment list, which arrives as an array
// normally but as an object keyed by index when the server filtered out some
// entries: Laravel serializes a collection with gaps in its keys that way.
type deploymentList []Deployment

func (l *deploymentList) UnmarshalJSON(data []byte) error {
var list []Deployment
if err := json.Unmarshal(data, &list); err == nil {
*l = list
return nil
}
var keyed map[string]Deployment
if err := json.Unmarshal(data, &keyed); err != nil {
return err
}
keys := make([]string, 0, len(keyed))
for k := range keyed {
keys = append(keys, k)
}
// Keep the server's order: keys are indexes, so compare them as numbers.
sort.Slice(keys, func(i, j int) bool {
if len(keys[i]) != len(keys[j]) {
return len(keys[i]) < len(keys[j])
}
return keys[i] < keys[j]
})
list = make([]Deployment, 0, len(keys))
for _, k := range keys {
list = append(list, keyed[k])
}
*l = list
return nil
}

type deploymentPage struct {
Count int `json:"count"`
Deployments []Deployment `json:"deployments"`
Count int `json:"count"`
Deployments deploymentList `json:"deployments"`
}

// LogLine is one entry from a deployment's log stream. Coolify stores these as
Expand Down
Loading