Skip to content
Draft
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
38 changes: 15 additions & 23 deletions cmd/resources.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package cmd

import (
"bytes"
"encoding/json"
"fmt"
"log"

"github.com/coollabsio/coolify-cli/pkg/client"
"github.com/coollabsio/coolify-cli/pkg/config"
"github.com/spf13/cobra"
)

Expand All @@ -18,34 +16,28 @@ var listResourcesCmd = &cobra.Command{
Use: "list",
Short: "List all resources",
Run: func(cmd *cobra.Command, args []string) {
// TODO - pull this apart once we are refactored to the client
CheckDefaultThings(nil)
data, err := Fetch("resources")
c := client.New(config.GetBaseUrl(), config.GetToken())

resources, err := c.ListResources()
if err != nil {
log.Println(err)
return
fmt.Println(cmd.ErrOrStderr(), err)
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")

// Format as JSON. TODO: Is this needed or should people prefer cURL/other at this stage?
if JsonMode || PrettyMode {
json, err := prettyJson(resources)
if err != nil {
fmt.Println(err)
fmt.Println(cmd.ErrOrStderr(), err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata []Resource
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
fmt.Println(json)
return
}

fmt.Fprintln(w, "Uuid\tName\tType\tStatus")
for _, resource := range jsondata {
for _, resource := range resources {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", resource.Uuid, resource.Name, resource.Type, resource.Status)
}
w.Flush()
Expand Down
14 changes: 14 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"bytes"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -350,3 +351,16 @@ func initConfig() {
log.Printf("New version of Coolify CLI is available: %s\n", data)
}
}

func prettyJson(obj interface{}) (string, error) {
var prettyJSON bytes.Buffer
marshal, err := json.Marshal(obj)
if err != nil {
return "", err
}
err = json.Indent(&prettyJSON, marshal, "", "\t")
if err != nil {
return "", err
}
return prettyJSON.String(), nil
}
12 changes: 3 additions & 9 deletions cmd/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,15 @@ import (
"fmt"
"log"

"github.com/coollabsio/coolify-cli/pkg/client"
"github.com/spf13/cobra"
)

var WithResources bool

type Resource struct {
ID int `json:"id"`
Uuid string `json:"uuid"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
}

type Resources struct {
Resources []Resource `json:"resources"`
// Transitional with refactor
Resources []client.Resource `json:"resources"`
}

type Server struct {
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ require (
github.com/hashicorp/go-version v1.7.0
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
// Test dependencies
github.com/stretchr/testify v1.10.0
)

require (
code.gitea.io/sdk/gitea v0.20.0 // indirect
github.com/42wim/httpsig v1.2.2 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
Expand All @@ -28,6 +31,7 @@ require (
github.com/magiconair/properties v1.8.9 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
Expand Down
70 changes: 70 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package client

import (
"encoding/json"
"fmt"
"io"
"net/http"
)

type CoolifyClient struct {
baseUrl string
token string
httpClient *http.Client
}

func New(baseUrl, token string) *CoolifyClient {
return &CoolifyClient{
baseUrl: baseUrl,
token: token,
httpClient: http.DefaultClient,
}
}

type JustMessage struct {
Message string `json:"message"`
}

func (c *CoolifyClient) buildPath(pathFromApiBase string) string {
return fmt.Sprintf("%s/api/v1/%s", c.baseUrl, pathFromApiBase)
}

func (c *CoolifyClient) doRequest(request *http.Request, mapToStruct interface{}) error {
request.Header.Set("Authorization", "Bearer "+c.token)
request.Header.Set("Accept", "application/json")

// Basic request and error handling
response, err := c.httpClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()

// A successful request should not be 400+
if response.StatusCode >= http.StatusBadRequest {
// Get the body
rawError, err := io.ReadAll(response.Body)
if err != nil {
return err
}
// It is probably a simple JSON message response
message := &JustMessage{}
err = json.Unmarshal(rawError, message)
if err != nil {
// It wasn't the error JSON message we were expecting, blast out the raw error
return fmt.Errorf("%s: %s", response.Status, rawError)
}
return fmt.Errorf("%s: %s", response.Status, message.Message)
}

// Marshal to JSON
rawBody, err := io.ReadAll(response.Body)
if err != nil {
return err
}
err = json.Unmarshal(rawBody, mapToStruct)
if err != nil {
return err
}
return nil
}
55 changes: 55 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package client_test

import (
"github.com/coollabsio/coolify-cli/pkg/client"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)

func TestClient_ReturnsAnErrorForBadHTTPCodes(t *testing.T) {
mockMux := http.NewServeMux()
mockMux.HandleFunc("/api/v1/resources", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("bang, a non json error"))
})
server := httptest.NewServer(mockMux)
defer server.Close()

c := client.New(server.URL, "my-token")

_, err := c.ListResources()

assert.ErrorContains(t, err, "bang, a non json error")
}

func TestClient_ReturnsAnErrorForBadHTTPCodes_andMarshalsTheCoolifyMessageIfItCan(t *testing.T) {
mockMux := http.NewServeMux()
mockMux.HandleFunc("/api/v1/resources", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message": "bang"}`))
})
server := httptest.NewServer(mockMux)
defer server.Close()

c := client.New(server.URL, "my-token")

_, err := c.ListResources()

assert.ErrorContains(t, err, "bang")
}

func TestClient_SetsAppropriateHeadersWhenPerformingRequest(t *testing.T) {
mockMux := http.NewServeMux()
mockMux.HandleFunc("/api/v1/resources", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer my-token", r.Header.Get("Authorization"))
assert.Equal(t, "application/json", r.Header.Get("Accept"))
})
server := httptest.NewServer(mockMux)
defer server.Close()

c := client.New(server.URL, "my-token")

_, _ = c.ListResources()
}
26 changes: 26 additions & 0 deletions pkg/client/resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package client

import "net/http"

type Resource struct {
ID int `json:"id"`
Uuid string `json:"uuid"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
}

func (c *CoolifyClient) ListResources() ([]Resource, error) {
request, err := http.NewRequest(http.MethodGet, c.buildPath("resources"), nil)
if err != nil {
return nil, err
}

var resources []Resource
err = c.doRequest(request, &resources)
if err != nil {
return nil, err
}

return resources, nil
}
31 changes: 31 additions & 0 deletions pkg/client/resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package client_test

import (
"github.com/coollabsio/coolify-cli/pkg/client"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"os"
"testing"
)

func TestClient_ListResources(t *testing.T) {
mockMux := http.NewServeMux()
mockMux.HandleFunc("/api/v1/resources", func(w http.ResponseWriter, r *http.Request) {
bytes, _ := os.ReadFile("resources_test.json")
w.Write(bytes)
})
server := httptest.NewServer(mockMux)
defer server.Close()

c := client.New(server.URL, "my-token")

resources, _ := c.ListResources()

assert.Len(t, resources, 1)
assert.Equal(t, "running:healthy", resources[0].Status)
assert.Equal(t, 2, resources[0].ID)
assert.Equal(t, "coollabsio/coolify-examples:v4.x-zxczxcxzc", resources[0].Name)
assert.Equal(t, "application", resources[0].Type)
assert.Equal(t, "abc123", resources[0].Uuid)
}
Loading