aboutsummaryrefslogtreecommitdiff
path: root/web/fly/flymachines
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2023-11-05 11:07:36 -0500
committerXe Iaso <me@xeiaso.net>2023-11-05 11:07:36 -0500
commit2b633ac6329b0a7bbc59a407a061b65a79eeee21 (patch)
tree6638dc98cfbb44cfaf2fb1eff1472af025e7572f /web/fly/flymachines
parentfc0a43c2e584fa701edcddc1d86747865a4a14ef (diff)
downloadx-2b633ac6329b0a7bbc59a407a061b65a79eeee21.tar.xz
x-2b633ac6329b0a7bbc59a407a061b65a79eeee21.zip
web/fly/flymachines: add volumes support
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'web/fly/flymachines')
-rw-r--r--web/fly/flymachines/apps.go79
-rw-r--r--web/fly/flymachines/client.go82
-rw-r--r--web/fly/flymachines/machines.go85
-rw-r--r--web/fly/flymachines/volumes.go102
4 files changed, 240 insertions, 108 deletions
diff --git a/web/fly/flymachines/apps.go b/web/fly/flymachines/apps.go
index 3f36c52..19e3b07 100644
--- a/web/fly/flymachines/apps.go
+++ b/web/fly/flymachines/apps.go
@@ -1,7 +1,6 @@
package flymachines
import (
- "bytes"
"context"
"encoding/json"
"fmt"
@@ -46,35 +45,12 @@ func (mt MilliTime) MarshalJSON() ([]byte, error) {
// CreateApp creates a single application in the given organization and on the given network.
func (c *Client) CreateApp(ctx context.Context, caa CreateAppArgs) (*CreateAppResponse, error) {
- var buf bytes.Buffer
- if err := json.NewEncoder(&buf).Encode(caa); err != nil {
- return nil, fmt.Errorf("flymachines: can't encode CreateApp request body: %w", err)
- }
-
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL+"/v1/apps", &buf)
+ result, err := doJSONBody[CreateAppArgs, CreateAppResponse](ctx, c, http.MethodPost, "/v1/apps", caa, http.StatusCreated)
if err != nil {
- return nil, fmt.Errorf("flymachines: can't create CreateApp request: %w", err)
- }
-
- req.Header.Set("Content-Type", "application/json")
-
- resp, err := c.Do(req)
- if err != nil {
- return nil, fmt.Errorf("flymachines: can't perform CreateApp request: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusCreated {
- return nil, web.NewError(http.StatusCreated, resp)
+ return nil, fmt.Errorf("flymachines: can't decode CreateApp response: %w", err)
}
- var car CreateAppResponse
-
- if err := json.NewDecoder(resp.Body).Decode(&car); err != nil {
- return nil, fmt.Errorf("flymachines: can't decode response body for CreateApp: %w", err)
- }
-
- return &car, nil
+ return &result, nil
}
// App is a Fly app. Apps are collections of resources such as machines, volumes, and IP addresses.
@@ -106,32 +82,12 @@ type Org struct {
// GetApps gets all of the applications in an organization.
func (c *Client) GetApps(ctx context.Context, orgSlug string) ([]ListApp, error) {
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiURL+"/v1/apps", nil)
- if err != nil {
- return nil, fmt.Errorf("flymachines: can't create GetApps request: %w", err)
- }
-
- q := req.URL.Query()
- q.Set("org_slug", orgSlug)
- req.URL.RawQuery = q.Encode()
-
- resp, err := c.Do(req)
- if err != nil {
- return nil, fmt.Errorf("flymachines: can't perform GetApps request: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return nil, web.NewError(http.StatusOK, resp)
- }
-
- var result struct {
+ result, err := doJSON[struct {
Apps []ListApp `json:"apps"`
TotalApps int `json:"total_apps"`
- }
-
- if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
- return nil, fmt.Errorf("flymachines: can't decode response body for GetApps: %w", err)
+ }](ctx, c, http.MethodGet, "/v1/apps?org_slug="+orgSlug, http.StatusOK)
+ if err != nil {
+ return nil, fmt.Errorf("flymachines: can't decode GetApps response: %w", err)
}
return result.Apps, nil
@@ -139,27 +95,12 @@ func (c *Client) GetApps(ctx context.Context, orgSlug string) ([]ListApp, error)
// GetApp fetches information about one app in particular.
func (c *Client) GetApp(ctx context.Context, appName string) (*SingleApp, error) {
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiURL+"/v1/apps/"+appName, nil)
+ result, err := doJSON[SingleApp](ctx, c, http.MethodGet, "/v1/apps/"+appName, http.StatusOK)
if err != nil {
- return nil, fmt.Errorf("flymachines: can't create GetApp request: %w", err)
- }
-
- resp, err := c.Do(req)
- if err != nil {
- return nil, fmt.Errorf("flymachines: can't perform GetApp request: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return nil, web.NewError(http.StatusOK, resp)
- }
-
- var app SingleApp
- if err := json.NewDecoder(resp.Body).Decode(&app); err != nil {
- return nil, fmt.Errorf("flymachines: can't decode response body for GetApp: %w", err)
+ return nil, fmt.Errorf("flymachines: can't decode GetApp response: %w", err)
}
- return &app, nil
+ return &result, nil
}
func (c *Client) DeleteApp(ctx context.Context, appName string) error {
diff --git a/web/fly/flymachines/client.go b/web/fly/flymachines/client.go
index 6fdcae4..30fc881 100644
--- a/web/fly/flymachines/client.go
+++ b/web/fly/flymachines/client.go
@@ -1,8 +1,13 @@
package flymachines
import (
+ "bytes"
+ "context"
+ "encoding/json"
"net/http"
"os"
+
+ "within.website/x/web"
)
const (
@@ -45,3 +50,80 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", "within.website/x/web/fly/flymachines in "+os.Args[0])
return c.cli.Do(req)
}
+
+func (c *Client) doRequestNoResponse(ctx context.Context, method, path string, wantStatusCode int) error {
+ req, err := http.NewRequestWithContext(ctx, method, c.apiURL+path, nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := c.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != wantStatusCode {
+ return web.NewError(wantStatusCode, resp)
+ }
+
+ return nil
+}
+
+// zilch returns the zero value of a given type.
+func zilch[T any]() T { return *new(T) }
+
+func doJSON[Output any](ctx context.Context, c *Client, method, path string, wantStatusCode int) (Output, error) {
+ req, err := http.NewRequestWithContext(ctx, method, c.apiURL+path, nil)
+ if err != nil {
+ return zilch[Output](), err
+ }
+
+ resp, err := c.Do(req)
+ if err != nil {
+ return zilch[Output](), err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != wantStatusCode {
+ return zilch[Output](), web.NewError(wantStatusCode, resp)
+ }
+
+ var output Output
+
+ if err := json.NewDecoder(resp.Body).Decode(&output); err != nil {
+ return zilch[Output](), err
+ }
+
+ return output, nil
+}
+
+func doJSONBody[Input any, Output any](ctx context.Context, c *Client, method, path string, input Input, wantStatusCode int) (Output, error) {
+ buf := new(bytes.Buffer)
+ if err := json.NewEncoder(buf).Encode(input); err != nil {
+ return zilch[Output](), err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, c.apiURL+path, buf)
+ if err != nil {
+ return zilch[Output](), err
+ }
+
+ resp, err := c.Do(req)
+ if err != nil {
+ return zilch[Output](), err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != wantStatusCode {
+ return zilch[Output](), web.NewError(wantStatusCode, resp)
+ }
+
+ var output Output
+
+ if err := json.NewDecoder(resp.Body).Decode(&output); err != nil {
+ return zilch[Output](), err
+ }
+
+ return output, nil
+}
diff --git a/web/fly/flymachines/machines.go b/web/fly/flymachines/machines.go
index 556cc45..bf01e72 100644
--- a/web/fly/flymachines/machines.go
+++ b/web/fly/flymachines/machines.go
@@ -1,7 +1,6 @@
package flymachines
import (
- "bytes"
"context"
"encoding/json"
"fmt"
@@ -52,11 +51,12 @@ type MachineService struct {
}
type MachineGuest struct {
- CPUKind string `json:"cpu_kind"` // "shared" or "performance"
- CPUs int `json:"cpus"`
- MemoryMB int `json:"memory_mb"`
- GPUKind *string `json:"gpu_kind,omitempty"`
- KernelArgs []string `json:"kernel_args,omitempty"`
+ CPUKind string `json:"cpu_kind"` // "shared" or "performance"
+ CPUs int `json:"cpus"`
+ MemoryMB int `json:"memory_mb"`
+ GPUKind string `json:"gpu_kind,omitempty"`
+ KernelArgs []string `json:"kernel_args,omitempty"`
+ HostDedicationID string `json:"host_dedication_id,omitempty"`
}
type MachineStopConfig struct {
@@ -173,54 +173,61 @@ type CreateMachine struct {
}
func (c *Client) CreateMachine(ctx context.Context, appID string, cm CreateMachine) (*Machine, error) {
- buf := new(bytes.Buffer)
- if err := json.NewEncoder(buf).Encode(cm); err != nil {
- return nil, fmt.Errorf("flymachines: can't encode CreateMachine request body: %w", err)
- }
-
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL+"/v1/apps/"+appID+"/machines", buf)
+ result, err := doJSONBody[CreateMachine, Machine](ctx, c, http.MethodPost, "/v1/apps/"+appID+"/machines", cm, http.StatusOK)
if err != nil {
- return nil, fmt.Errorf("flymachines: can't create CreateMachine request: %w", err)
+ return nil, fmt.Errorf("flymachines: can't decode CreateMachine response: %w", err)
}
- resp, err := c.Do(req)
+ return &result, nil
+}
+
+func (c *Client) GetAppMachine(ctx context.Context, appID, machineID string) (*Machine, error) {
+ result, err := doJSON[Machine](ctx, c, http.MethodGet, "/v1/apps/"+appID+"/machines/"+machineID, http.StatusOK)
if err != nil {
- return nil, fmt.Errorf("flymachines: can't do CreateMachine request: %w", err)
+ return nil, fmt.Errorf("flymachines: can't decode GetAppMachine response: %w", err)
}
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("flymachines: CreateMachine request failed: %s", resp.Status)
- }
+ return &result, nil
+}
- var machine Machine
- if err := json.NewDecoder(resp.Body).Decode(&machine); err != nil {
- return nil, fmt.Errorf("flymachines: can't decode CreateMachine response: %w", err)
- }
+func (c *Client) DeleteAppMachine(ctx context.Context, appID, machineID string) error {
+ return c.doRequestNoResponse(ctx, http.MethodDelete, "/v1/apps/"+appID+"/machines/"+machineID, http.StatusOK)
+}
- return &machine, nil
+func (c *Client) CordonAppMachine(ctx context.Context, appID, machineID string) error {
+ return c.doRequestNoResponse(ctx, http.MethodPost, "/v1/apps/"+appID+"/machines/"+machineID+"/cordon", http.StatusOK)
}
-func (c *Client) GetAppMachine(ctx context.Context, appID, machineID string) (*Machine, error) {
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiURL+"/v1/apps/"+appID+"/machines/"+machineID, nil)
- if err != nil {
- return nil, fmt.Errorf("flymachines: can't create GetMachine request: %w", err)
- }
+func (c *Client) UncordonAppMachine(ctx context.Context, appID, machineID string) error {
+ return c.doRequestNoResponse(ctx, http.MethodPost, "/v1/apps/"+appID+"/machines/"+machineID+"/uncordon", http.StatusOK)
+}
- resp, err := c.Do(req)
+func (c *Client) StartAppMachine(ctx context.Context, appID, machineID string) error {
+ return c.doRequestNoResponse(ctx, http.MethodPost, "/v1/apps/"+appID+"/machines/"+machineID+"/start", http.StatusOK)
+}
+
+func (c *Client) StopAppMachine(ctx context.Context, appID, machineID string) error {
+ return c.doRequestNoResponse(ctx, http.MethodPost, "/v1/apps/"+appID+"/machines/"+machineID+"/stop", http.StatusOK)
+}
+
+func (c *Client) RestartAppMachine(ctx context.Context, appID, machineID string) error {
+ return c.doRequestNoResponse(ctx, http.MethodPost, "/v1/apps/"+appID+"/machines/"+machineID+"/restart", http.StatusOK)
+}
+
+func (c *Client) GetAppMachineEvents(ctx context.Context, appID, machineID string) ([]MachineEvent, error) {
+ result, err := doJSON[[]MachineEvent](ctx, c, http.MethodGet, "/v1/apps/"+appID+"/machines/"+machineID+"/events", http.StatusOK)
if err != nil {
- return nil, fmt.Errorf("flymachines: can't do GetMachine request: %w", err)
+ return nil, fmt.Errorf("flymachines: can't decode GetAppMachineEvents response: %w", err)
}
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("flymachines: GetMachine request failed: %s", resp.Status)
- }
+ return result, nil
+}
- var machine Machine
- if err := json.NewDecoder(resp.Body).Decode(&machine); err != nil {
- return nil, fmt.Errorf("flymachines: can't decode GetMachine response: %w", err)
+func (c *Client) GetAppMachineMetadata(ctx context.Context, appID, machineID string) (map[string]string, error) {
+ result, err := doJSON[map[string]string](ctx, c, http.MethodGet, "/v1/apps/"+appID+"/machines/"+machineID+"/metadata", http.StatusOK)
+ if err != nil {
+ return nil, fmt.Errorf("flymachines: can't decode GetAppMachineMetadata response: %w", err)
}
- return &machine, nil
+ return result, nil
}
diff --git a/web/fly/flymachines/volumes.go b/web/fly/flymachines/volumes.go
new file mode 100644
index 0000000..8f2563d
--- /dev/null
+++ b/web/fly/flymachines/volumes.go
@@ -0,0 +1,102 @@
+package flymachines
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+type Volume struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ State string `json:"state"`
+ SizeGB int `json:"size_gb"`
+ Region string `json:"region"`
+ Zone string `json:"zone"`
+ Encrypted bool `json:"encrypted"`
+ AttachedAllocID string `json:"attached_alloc_id"`
+ AttachedMachineID string `json:"attached_machine_id"`
+ CreatedAt time.Time `json:"created_at"`
+ Blocks int `json:"blocks"`
+ BlockSize int `json:"block_size"`
+ BlocksAvail int `json:"blocks_avail"`
+ BlocksFree int `json:"blocks_free"`
+ FSType string `json:"fs_type"`
+ SnapshotRetention int `json:"snapshot_retention"`
+ HostDedicationKey string `json:"host_dedication_key,omitempty"`
+}
+
+func (c *Client) GetVolumes(ctx context.Context, appName string) ([]Volume, error) {
+ return doJSON[[]Volume](ctx, c, http.MethodGet, "/v1/apps/"+appName+"/volumes", http.StatusOK)
+}
+
+func (c *Client) GetVolume(ctx context.Context, appName, volumeID string) (*Volume, error) {
+ result, err := doJSON[Volume](ctx, c, http.MethodGet, "/v1/apps/"+appName+"/volumes/"+volumeID, http.StatusOK)
+ if err != nil {
+ return nil, fmt.Errorf("flymachines: can't decode GetVolume response: %w", err)
+ }
+
+ return &result, nil
+}
+
+type CreateVolume struct {
+ Compute *MachineGuest `json:"compute,omitempty"`
+ Encrypted bool `json:"encrypted,omitempty"`
+ FSType string `json:"fs_type,omitempty"`
+ MachinesOnly bool `json:"machines_only,omitempty"`
+ Name string `json:"name,omitempty"`
+ Region string `json:"region,omitempty"`
+ RequireUniqueZone bool `json:"require_unique_zone"`
+ SizeGB int `json:"size_gb"`
+ SnapshotID string `json:"snapshot_id,omitempty"`
+ SnapshotRetention int `json:"snapshot_retention"`
+ SourceVolumeID string `json:"source_volume_id,omitempty"`
+}
+
+func (c *Client) CreateVolume(ctx context.Context, appName string, cv CreateVolume) (*Volume, error) {
+ result, err := doJSONBody[CreateVolume, Volume](ctx, c, http.MethodPost, "/v1/apps/"+appName+"/volumes", cv, http.StatusOK)
+ if err != nil {
+ return nil, fmt.Errorf("flymachines: can't decode CreateVolume response: %w", err)
+ }
+
+ return &result, nil
+}
+
+func (c *Client) DeleteVolume(ctx context.Context, appName, volumeID string) error {
+ _, err := doJSON[Volume](ctx, c, http.MethodDelete, "/v1/apps/"+appName+"/volumes/"+volumeID, http.StatusNoContent)
+ if err != nil {
+ return fmt.Errorf("flymachines: can't decode DeleteVolume response: %w", err)
+ }
+
+ return nil
+}
+
+type ExtendVolumeResponse struct {
+ NeedsRestart bool `json:"needs_restart"`
+ Volume Volume `json:"volume"`
+}
+
+func (c *Client) ExtendVolume(ctx context.Context, appName, voluleID string, sizeGB int) (*ExtendVolumeResponse, error) {
+ type req struct {
+ SizeGB int `json:"size_gb"`
+ }
+
+ result, err := doJSONBody[req, ExtendVolumeResponse](ctx, c, http.MethodPost, "/v1/apps/"+appName+"/volumes/"+voluleID+"/extend", req{sizeGB}, http.StatusOK)
+ if err != nil {
+ return nil, fmt.Errorf("flymachines: can't decode ExtendVolume response: %w", err)
+ }
+
+ return &result, nil
+}
+
+type Snapshot struct {
+ CreatedAt time.Time `json:"created_at"`
+ Digest string `json:"digest"`
+ ID string `json:"id"`
+ Size int `json:"size"`
+}
+
+func (c *Client) ListVolumeSnapshots(ctx context.Context, appName, volumeID string) ([]Snapshot, error) {
+ return doJSON[[]Snapshot](ctx, c, http.MethodGet, "/v1/apps/"+appName+"/volumes/"+volumeID+"/snapshots", http.StatusOK)
+}