diff options
| author | Xe Iaso <me@xeiaso.net> | 2023-11-05 11:07:36 -0500 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2023-11-05 11:07:36 -0500 |
| commit | 2b633ac6329b0a7bbc59a407a061b65a79eeee21 (patch) | |
| tree | 6638dc98cfbb44cfaf2fb1eff1472af025e7572f /web/fly/flymachines | |
| parent | fc0a43c2e584fa701edcddc1d86747865a4a14ef (diff) | |
| download | x-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.go | 79 | ||||
| -rw-r--r-- | web/fly/flymachines/client.go | 82 | ||||
| -rw-r--r-- | web/fly/flymachines/machines.go | 85 | ||||
| -rw-r--r-- | web/fly/flymachines/volumes.go | 102 |
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) +} |
