From b02a901a05694539244f74308008b161e66c680c Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Thu, 7 Sep 2023 18:11:57 +0800 Subject: [PATCH] feat: add generated types and more machine apis --- README.md | 20 +- __tests__/app.test.ts | 110 +++----- __tests__/machine.test.ts | 249 +++++++++++++++-- __tests__/network.test.ts | 2 +- __tests__/organization.test.ts | 2 +- __tests__/secret.test.ts | 2 +- __tests__/volume.test.ts | 52 ++-- jest.config.js | 7 +- package-lock.json | 17 +- package.json | 2 + src/client.ts | 12 +- src/lib/app.ts | 117 +++----- src/lib/machine.ts | 497 +++++++++++++++++++++------------ src/lib/types.ts | 460 ++++++++++++++++++++++++++++++ src/lib/volume.ts | 54 ++-- 15 files changed, 1198 insertions(+), 405 deletions(-) create mode 100644 src/lib/types.ts diff --git a/README.md b/README.md index 8f96e28..4f0d692 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,17 @@ async function deployApp() { - `fly.Machine.updateMachine()` - `fly.Machine.startMachine()` - `fly.Machine.stopMachine()` -- `fly.Machine.restartMachine()` - `fly.Machine.deleteMachine()` +- `fly.Machine.restartMachine()` +- `fly.Machine.signalMachine()` +- `fly.Machine.waitMachine()` +- `fly.Machine.cordonMachine()` +- `fly.Machine.uncordonMachine()` +- `fly.Machine.listEvents()` +- `fly.Machine.listVersions()` +- `fly.Machine.listProcesses()` +- `fly.Machine.getLease()` +- `fly.Machine.acquireLease()` **Networks** @@ -61,6 +70,15 @@ async function deployApp() { - `fly.Volume.createVolume()` - `fly.Volume.deleteVolume()` - `fly.Volume.extendVolume()` +- `fly.Volume.listSnapshots()` + +**TODO** + +- [ ] `fly.Machine.execMachine()` +- [ ] `fly.Machine.releaseLease()` +- [ ] `fly.Machine.getMetadata()` +- [ ] `fly.Machine.updateMetadata()` +- [ ] `fly.Machine.deleteMetadata()` ## License diff --git a/__tests__/app.test.ts b/__tests__/app.test.ts index 0da3e7b..4c63bec 100644 --- a/__tests__/app.test.ts +++ b/__tests__/app.test.ts @@ -1,83 +1,61 @@ import nock from 'nock' import { describe, it } from '@jest/globals' -import { FLY_API_GRAPHQL } from '../src/client' +import { FLY_API_HOSTNAME } from '../src/client' import { createClient } from '../src/main' +import { AppResponse, AppStatus } from '../src/lib/app' -const fly = createClient('test-token') +const fly = createClient(process.env.FLY_API_TOKEN || 'test-token') describe('app', () => { - const organizationId = 'personal' + const app: AppResponse = { + name: 'fly-app', + status: AppStatus.deployed, + organization: { + name: 'fly-org', + slug: 'personal', + }, + } - it('creates app', async () => { - nock(FLY_API_GRAPHQL) - .post('/graphql') + it('lists apps', async () => { + const org_slug = app.organization.slug + nock(FLY_API_HOSTNAME) + .get('/v1/apps') + .query({ org_slug }) .reply(200, { - data: { - createApp: { - app: { - id: 'ctwntjgykzxhfncfwrfo', - name: 'ctwntjgykzxhfncfwrfo', - organization: { slug: 'supabase-dev' }, - config: { - definition: { - kill_timeout: 5, - kill_signal: 'SIGINT', - processes: [], - experimental: { auto_rollback: true }, - services: [ - { - processes: ['app'], - protocol: 'tcp', - internal_port: 8080, - concurrency: { - soft_limit: 20, - hard_limit: 25, - type: 'connections', - }, - ports: [ - { port: 80, handlers: ['http'], force_https: true }, - { port: 443, handlers: ['tls', 'http'] }, - ], - tcp_checks: [ - { - interval: '15s', - timeout: '2s', - grace_period: '1s', - restart_limit: 0, - }, - ], - http_checks: [], - script_checks: [], - }, - ], - env: {}, - }, - }, - regions: [{ name: 'Hong Kong, Hong Kong', code: 'hkg' }], - }, + total_apps: 1, + apps: [ + { + name: app.name, + machine_count: 1, + network: 'default', }, - }, + ], }) - const data = await fly.App.createApp({ - name: 'ctwntjgykzxhfncfwrfo', - organizationId, - }) + const data = await fly.App.listApps(org_slug) + console.dir(data, { depth: 10 }) + }) + + it('get app', async () => { + const app_name = app.name + nock(FLY_API_HOSTNAME).get(`/v1/apps/${app_name}`).reply(200, app) + const data = await fly.App.getApp(app_name) + console.dir(data, { depth: 10 }) + }) + + it('creates app', async () => { + const body = { + org_slug: app.organization.slug, + app_name: app.name, + } + nock(FLY_API_HOSTNAME).post('/v1/apps', body).reply(201) + const data = await fly.App.createApp(body) console.dir(data, { depth: 10 }) }) it('deletes app', async () => { - nock(FLY_API_GRAPHQL) - .post('/graphql') - .reply(200, { - data: { - deleteApp: { - organization: { - id: organizationId, - }, - }, - }, - }) - const data = await fly.App.deleteApp('ctwntjgykzxhfncfwrfo') + const app_name = app.name + nock(FLY_API_HOSTNAME).delete(`/v1/apps/${app_name}`).reply(202) + const data = await fly.App.deleteApp(app_name) console.dir(data, { depth: 10 }) }) }) diff --git a/__tests__/machine.test.ts b/__tests__/machine.test.ts index bbc7664..e06ef76 100644 --- a/__tests__/machine.test.ts +++ b/__tests__/machine.test.ts @@ -8,11 +8,12 @@ import { } from '../src/lib/machine' import { FLY_API_HOSTNAME } from '../src/client' import { createClient } from '../src/main' +import { SignalRequestSignalEnum, StateEnum } from '../src/lib/types' -const fly = createClient('test-token') +const fly = createClient(process.env.FLY_API_TOKEN || 'test-token') describe('machine', () => { - const appId = 'ctwntjgykzxhfncfwrfo' + const app_name = 'ctwntjgykzxhfncfwrfo' const machine: MachineResponse = { id: '17811953c92e18', name: 'ctwntjgykzxhfncfwrfo', @@ -174,13 +175,13 @@ describe('machine', () => { }, } nock(FLY_API_HOSTNAME) - .post(`/v1/apps/${appId}/machines`, { + .post(`/v1/apps/${app_name}/machines`, { name: machine.name, config: config as any, }) .reply(200, machine) const data = await fly.Machine.createMachine({ - appId, + app_name, name: machine.name, config, }) @@ -188,72 +189,72 @@ describe('machine', () => { }) it('updates machine', async () => { - const machineId = machine.id + const machine_id = machine.id const config = { image: 'sweatybridge/postgres:all-in-one', services: [], } nock(FLY_API_HOSTNAME) - .post(`/v1/apps/${appId}/machines/${machineId}`, { config }) + .post(`/v1/apps/${app_name}/machines/${machine_id}`, { config }) .reply(200, machine) const data = await fly.Machine.updateMachine({ - appId, - machineId, + app_name, + machine_id, config, }) console.dir(data, { depth: 5 }) }) it('deletes machine', async () => { - const machineId = machine.id + const machine_id = machine.id nock(FLY_API_HOSTNAME) - .delete(`/v1/apps/${appId}/machines/${machineId}`) + .delete(`/v1/apps/${app_name}/machines/${machine_id}`) .reply(200, { ok: true }) const data = await fly.Machine.deleteMachine({ - appId, - machineId, + app_name, + machine_id, }) console.dir(data, { depth: 5 }) }) it('stops machine', async () => { - const machineId = machine.id + const machine_id = machine.id nock(FLY_API_HOSTNAME) - .post(`/v1/apps/${appId}/machines/${machineId}/stop`, { + .post(`/v1/apps/${app_name}/machines/${machine_id}/stop`, { signal: 'SIGTERM', }) .reply(200, { ok: true }) const data = await fly.Machine.stopMachine({ - appId, - machineId, + app_name, + machine_id, }) console.dir(data, { depth: 5 }) }) it('starts machine', async () => { - const machineId = machine.id + const machine_id = machine.id nock(FLY_API_HOSTNAME) - .post(`/v1/apps/${appId}/machines/${machineId}/start`) + .post(`/v1/apps/${app_name}/machines/${machine_id}/start`) .reply(200, { ok: true }) const data = await fly.Machine.startMachine({ - appId, - machineId, + app_name, + machine_id, }) console.dir(data, { depth: 5 }) }) it('lists machines', async () => { nock(FLY_API_HOSTNAME) - .get(`/v1/apps/${appId}/machines`) + .get(`/v1/apps/${app_name}/machines`) .reply(200, [machine]) - const data = await fly.Machine.listMachines(appId) + const data = await fly.Machine.listMachines(app_name) console.dir(data, { depth: 10 }) }) - it('gets machines', async () => { - const machineId = machine.id + it('gets machine', async () => { + const machine_id = machine.id nock(FLY_API_HOSTNAME) - .get(`/v1/apps/${appId}/machines/${machineId}`) + .get(`/v1/apps/${app_name}/machines/${machine_id}`) .reply(200, { ...machine, state: MachineState.Started, @@ -268,7 +269,203 @@ describe('machine', () => { ...machine.events, ], }) - const data = await fly.Machine.getMachine({ appId, machineId }) + const data = await fly.Machine.getMachine({ + app_name, + machine_id, + }) + console.dir(data, { depth: 10 }) + }) + + it('restarts machine', async () => { + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .post(`/v1/apps/${app_name}/machines/${machine_id}/restart`) + .reply(200, { ok: true }) + const data = await fly.Machine.restartMachine({ + app_name, + machine_id, + }) + console.dir(data, { depth: 10 }) + }) + + it('signals machine', async () => { + const machine_id = machine.id + const signal = SignalRequestSignalEnum.SIGHUP + nock(FLY_API_HOSTNAME) + .post(`/v1/apps/${app_name}/machines/${machine_id}/signal`, { signal }) + .reply(200, { ok: true }) + const data = await fly.Machine.signalMachine({ + app_name, + machine_id, + signal, + }) + console.dir(data, { depth: 10 }) + }) + + it('waits machine', async () => { + const machine_id = machine.id + const state = StateEnum.Started + nock(FLY_API_HOSTNAME) + .get(`/v1/apps/${app_name}/machines/${machine_id}/wait`) + .query({ state }) + .reply(200, { ok: true }) + const data = await fly.Machine.waitMachine({ + app_name, + machine_id, + state, + }) + console.dir(data, { depth: 10 }) + }) + + it('lists events', async () => { + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .get(`/v1/apps/${app_name}/machines/${machine_id}/events`) + .reply(200, [ + { + id: '01H9QFJCZ03MEGZKYPYY4ZSTTS', + type: 'exit', + status: 'stopped', + request: { + exit_event: { + requested_stop: true, + restarting: false, + guest_exit_code: 0, + guest_signal: -1, + guest_error: '', + exit_code: 143, + signal: -1, + error: '', + oom_killed: false, + exited_at: '2023-09-07T09:28:59.832Z', + }, + restart_count: 0, + }, + source: 'flyd', + timestamp: 1694078940128, + }, + ]) + const data = await fly.Machine.listEvents({ + app_name, + machine_id, + }) + console.dir(data, { depth: 10 }) + }) + + it('lists versions', async () => { + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .get(`/v1/apps/${app_name}/machines/${machine_id}/versions`) + .reply(200, [ + { + version: '01H28X6YK062GWVQHQ6CF8FG5S', + user_config: machine.config, + }, + ]) + const data = await fly.Machine.listVersions({ + app_name, + machine_id, + }) + console.dir(data, { depth: 10 }) + }) + + it('lists processes', async () => { + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .get(`/v1/apps/${app_name}/machines/${machine_id}/ps`) + .reply(200, [ + { + pid: 713, + stime: 2, + rtime: 847, + command: 'nginx: worker process', + directory: '/', + cpu: 0, + rss: 97005568, + listen_sockets: [ + { proto: 'tcp', address: '0.0.0.0:8000' }, + { proto: 'tcp', address: '0.0.0.0:8443' }, + ], + }, + ]) + const data = await fly.Machine.listProcesses({ + app_name, + machine_id, + }) + console.dir(data, { depth: 10 }) + }) + + it('gets lease', async () => { + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .get(`/v1/apps/${app_name}/machines/${machine_id}/lease`) + .reply(200, { + status: 'success', + data: { + nonce: '45b8f9200c72', + expires_at: 1694080223, + owner: 'example@fly.io', + description: '', + }, + }) + const data = await fly.Machine.getLease({ + app_name, + machine_id, + }) + console.dir(data, { depth: 10 }) + }) + + it('acquires lease', async () => { + const body = { ttl: 60 } + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .post(`/v1/apps/${app_name}/machines/${machine_id}/lease`, body) + .reply(200, { + status: 'success', + data: { + nonce: '45b8f9200c72', + expires_at: 1694080223, + owner: 'example@fly.io', + description: '', + }, + }) + const data = await fly.Machine.acquireLease({ + app_name, + machine_id, + ...body, + }) + console.dir(data, { depth: 10 }) + }) + + it('cordons machine', async () => { + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .post(`/v1/apps/${app_name}/machines/${machine_id}/cordon`) + .reply(200, { + status: 'success', + data: { + nonce: '45b8f9200c72', + expires_at: 1694080223, + owner: 'example@fly.io', + description: '', + }, + }) + const data = await fly.Machine.cordonMachine({ + app_name, + machine_id, + }) + console.dir(data, { depth: 10 }) + }) + + it('uncordons machine', async () => { + const machine_id = machine.id + nock(FLY_API_HOSTNAME) + .post(`/v1/apps/${app_name}/machines/${machine_id}/uncordon`) + .reply(200, { ok: true }) + const data = await fly.Machine.uncordonMachine({ + app_name, + machine_id, + }) console.dir(data, { depth: 10 }) }) }) diff --git a/__tests__/network.test.ts b/__tests__/network.test.ts index 220d453..8ed9adf 100644 --- a/__tests__/network.test.ts +++ b/__tests__/network.test.ts @@ -4,7 +4,7 @@ import { AddressType } from '../src/lib/network' import { FLY_API_GRAPHQL } from '../src/client' import { createClient } from '../src/main' -const fly = createClient('test-token') +const fly = createClient(process.env.FLY_API_TOKEN || 'test-token') describe('network', () => { it('allocates ip address', async () => { diff --git a/__tests__/organization.test.ts b/__tests__/organization.test.ts index 7c720de..6a58a2c 100644 --- a/__tests__/organization.test.ts +++ b/__tests__/organization.test.ts @@ -3,7 +3,7 @@ import { describe, it } from '@jest/globals' import { FLY_API_GRAPHQL } from '../src/client' import { createClient } from '../src/main' -const fly = createClient('test-token') +const fly = createClient(process.env.FLY_API_TOKEN || 'test-token') describe('organization', () => { it('get personal', async () => { diff --git a/__tests__/secret.test.ts b/__tests__/secret.test.ts index 8a359f3..aa429d5 100644 --- a/__tests__/secret.test.ts +++ b/__tests__/secret.test.ts @@ -3,7 +3,7 @@ import { describe, it } from '@jest/globals' import { FLY_API_GRAPHQL } from '../src/client' import { createClient } from '../src/main' -const fly = createClient('test-token') +const fly = createClient(process.env.FLY_API_TOKEN || 'test-token') describe('secret', () => { it('sets secret', async () => { diff --git a/__tests__/volume.test.ts b/__tests__/volume.test.ts index 55345c0..1dd66fe 100644 --- a/__tests__/volume.test.ts +++ b/__tests__/volume.test.ts @@ -4,10 +4,10 @@ import { FLY_API_HOSTNAME } from '../src/client' import { createClient } from '../src/main' import { VolumeResponse } from '../src/lib/volume' -const fly = createClient('test-token') +const fly = createClient(process.env.FLY_API_TOKEN || 'test-token') describe('volume', () => { - const appId = 'ctwntjgykzxhfncfwrfo' + const app_name = 'ctwntjgykzxhfncfwrfo' const volume: VolumeResponse = { id: 'vol_0vdyzpeqgpzl383v', name: 'pgdata', @@ -29,7 +29,7 @@ describe('volume', () => { it('lists volumes', async () => { nock(FLY_API_HOSTNAME) - .get(`/v1/apps/${appId}/volumes`) + .get(`/v1/apps/${app_name}/volumes`) .reply(200, [ { ...volume, @@ -41,18 +41,18 @@ describe('volume', () => { fstype: 'ext4', }, ]) - const data = await fly.Volume.listVolumes(appId) + const data = await fly.Volume.listVolumes(app_name) console.dir(data, { depth: 5 }) }) it('gets volume', async () => { - const volumeId = volume.id + const volume_id = volume.id nock(FLY_API_HOSTNAME) - .get(`/v1/apps/${appId}/volumes/${volumeId}`) + .get(`/v1/apps/${app_name}/volumes/${volume_id}`) .reply(200, volume) const data = await fly.Volume.getVolume({ - appId, - volumeId, + app_name, + volume_id, }) console.dir(data, { depth: 5 }) }) @@ -64,26 +64,26 @@ describe('volume', () => { size_gb: 2, } nock(FLY_API_HOSTNAME) - .post(`/v1/apps/${appId}/volumes`, body) + .post(`/v1/apps/${app_name}/volumes`, body) .reply(200, volume) const data = await fly.Volume.createVolume({ - appId, + app_name, ...body, }) console.dir(data, { depth: 5 }) }) it('deletes volume', async () => { - const volumeId = volume.id + const volume_id = volume.id nock(FLY_API_HOSTNAME) - .delete(`/v1/apps/${appId}/volumes/${volumeId}`) + .delete(`/v1/apps/${app_name}/volumes/${volume_id}`) .reply(200, { ...volume, state: 'destroyed', }) const data = await fly.Volume.deleteVolume({ - appId, - volumeId, + app_name, + volume_id, }) console.dir(data, { depth: 5 }) }) @@ -95,7 +95,7 @@ describe('volume', () => { source_volume_id: volume.id, } nock(FLY_API_HOSTNAME) - .post(`/v1/apps/${appId}/volumes`, body) + .post(`/v1/apps/${app_name}/volumes`, body) .reply(200, { ...volume, id: 'vol_5456e1j33p16378r', @@ -103,7 +103,7 @@ describe('volume', () => { state: 'hydrating', }) const data = await fly.Volume.createVolume({ - appId, + app_name, ...body, }) console.dir(data, { depth: 5 }) @@ -111,9 +111,9 @@ describe('volume', () => { it('extends volume', async () => { const body = { size_gb: 4 } - const volumeId = volume.id + const volume_id = volume.id nock(FLY_API_HOSTNAME) - .put(`/v1/apps/${appId}/volumes/${volumeId}/extend`, body) + .put(`/v1/apps/${app_name}/volumes/${volume_id}/extend`, body) .reply(200, { needs_restart: true, volume: { @@ -122,10 +122,22 @@ describe('volume', () => { }, }) const data = await fly.Volume.extendVolume({ - appId, - volumeId, + app_name, + volume_id, ...body, }) console.dir(data, { depth: 5 }) }) + + it('lists snapshots', async () => { + const volume_id = volume.id + nock(FLY_API_HOSTNAME) + .get(`/v1/apps/${app_name}/volumes/${volume_id}/snapshots`) + .reply(200, []) + const data = await fly.Volume.listSnapshots({ + app_name, + volume_id, + }) + console.dir(data, { depth: 5 }) + }) }) diff --git a/jest.config.js b/jest.config.js index 5a82228..eee048f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,10 @@ module.exports = { clearMocks: true, moduleFileExtensions: ['js', 'ts'], + setupFiles: ['dotenv/config'], testMatch: ['**/*.test.ts'], transform: { - '^.+\\.ts$': 'ts-jest' + '^.+\\.ts$': 'ts-jest', }, - verbose: true -} \ No newline at end of file + verbose: true, +} diff --git a/package-lock.json b/package-lock.json index d10079e..69c689b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fly-admin", - "version": "0.0.1", + "version": "0.0.0-automated", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fly-admin", - "version": "0.0.1", + "version": "0.0.0-automated", "license": "MIT", "dependencies": { "cross-fetch": "^3.1.5" @@ -14,6 +14,7 @@ "devDependencies": { "@types/node": "^18.15.0", "@typescript-eslint/parser": "^5.58.0", + "dotenv": "^16.3.1", "eslint": "^8.38.0", "eslint-plugin-github": "^4.7.0", "eslint-plugin-jest": "^27.2.1", @@ -2281,6 +2282,18 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.411", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.411.tgz", diff --git a/package.json b/package.json index f02a474..6f7beb8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "format-check": "prettier --check '**/*.ts'", "lint": "eslint src/**/*.ts", "test": "jest", + "gen:types": "npx swagger-typescript-api -p https://docs.machines.dev/swagger/doc.json -o ./src/lib --extract-enums --extract-request-params --no-client -n types.ts", "all": "npm run build && npm run format && npm run lint && npm test" }, "repository": { @@ -32,6 +33,7 @@ "devDependencies": { "@types/node": "^18.15.0", "@typescript-eslint/parser": "^5.58.0", + "dotenv": "^16.3.1", "eslint": "^8.38.0", "eslint-plugin-github": "^4.7.0", "eslint-plugin-jest": "^27.2.1", diff --git a/src/client.ts b/src/client.ts index 2fe900f..3d85792 100644 --- a/src/client.ts +++ b/src/client.ts @@ -25,6 +25,11 @@ interface GraphQLResponse { }[] } +interface ClientConfig { + graphqlUrl?: string + apiUrl?: string +} + class Client { private graphqlUrl: string private apiUrl: string @@ -36,10 +41,7 @@ class Client { Secret: Secret Volume: Volume - constructor( - apiKey: string, - { graphqlUrl, apiUrl }: { graphqlUrl?: string; apiUrl?: string } = {} - ) { + constructor(apiKey: string, { graphqlUrl, apiUrl }: ClientConfig = {}) { if (!apiKey) { throw new Error('Fly API Key is required') } @@ -104,7 +106,7 @@ class Client { if (!resp.ok) { throw new Error(`${resp.status}: ${text}`) } - return JSON.parse(text) + return text ? JSON.parse(text) : undefined } } diff --git a/src/lib/app.ts b/src/lib/app.ts index 541e93a..52c6163 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -1,76 +1,40 @@ import Client from '../client' -export interface CreateAppInput { - organizationId: string - name?: string - preferredRegion?: string - network?: string -} +export type ListAppRequest = string -export interface CreateAppOutput { - createApp: { - app: { - id: string - name: string - organization: { - slug: string - } - config: { - definition: { - kill_timeout: number - kill_signal: string - processes: any[] - experimental: { - auto_rollback: boolean - } - services: any[] - env: Record - } - } - regions: { - name: string - code: string - }[] - } - } +export interface ListAppResponse { + total_apps: number + apps: { + name: string + machine_count: number + network: string + }[] } -const createAppQuery = `mutation($input: CreateAppInput!) { - createApp(input: $input) { - app { - id - name - organization { - slug - } - config { - definition - } - regions { - name - code - } - } - } -}` +export type GetAppRequest = string -export type DeleteAppInput = string +export enum AppStatus { + deployed = 'deployed', + pending = 'pending', + suspended = 'suspended', +} -export interface DeleteAppOutput { - deleteApp: { - organization: { - id: string - } +export interface AppResponse { + name: string + status: AppStatus + organization: { + name: string + slug: string } } -const deleteAppQuery = `mutation($appId: ID!) { - deleteApp(appId: $appId) { - organization { - id - } - } -}` +export interface CreateAppRequest { + org_slug: string + app_name: string + network?: string +} + +export type DeleteAppRequest = string export class App { private client: Client @@ -79,18 +43,23 @@ export class App { this.client = client } - async deleteApp(appId: DeleteAppInput): Promise { - return await this.client.gqlPostOrThrow({ - query: deleteAppQuery, - variables: { appId }, - }) + async listApps(org_slug: ListAppRequest): Promise { + const path = `apps?org_slug=${org_slug}` + return await this.client.restOrThrow(path) + } + + async getApp(app_name: GetAppRequest): Promise { + const path = `apps/${app_name}` + return await this.client.restOrThrow(path) + } + + async createApp(payload: CreateAppRequest): Promise { + const path = 'apps' + return await this.client.restOrThrow(path, 'POST', payload) } - // Ref: https://github.com/superfly/flyctl/blob/master/api/resource_apps.go#L329 - async createApp(input: CreateAppInput): Promise { - return await this.client.gqlPostOrThrow({ - query: createAppQuery, - variables: { input }, - }) + async deleteApp(app_name: DeleteAppRequest): Promise { + const path = `apps/${app_name}` + return await this.client.restOrThrow(path, 'DELETE') } } diff --git a/src/lib/machine.ts b/src/lib/machine.ts index 567dd11..704e7db 100644 --- a/src/lib/machine.ts +++ b/src/lib/machine.ts @@ -1,4 +1,111 @@ import Client from '../client' +import { + ApiMachineConfig, + ApiMachineInit, + ApiMachineService, + ApiMachineMount, + ApiMachinePort, + ApiMachineCheck, + ApiMachineRestart, + ApiMachineGuest, + CheckStatus as ApiCheckStatus, + CreateMachineRequest as ApiCreateMachineRequest, + ImageRef as ApiImageRef, + Machine as ApiMachine, + StateEnum as ApiMachineState, + SignalRequestSignalEnum as ApiMachineSignal, +} from './types' + +// We override the generated types from openapi spec to mark fields as non-optional +export interface MachineConfig extends ApiMachineConfig { + // The Docker image to run + image: string + // Optionally one of hourly, daily, weekly, monthly. Runs machine at the given interval. Interval starts at time of machine creation + schedule?: 'hourly' | 'daily' | 'weekly' | 'monthly' +} + +export type ListMachineRequest = + | string + | { + app_name: string + include_deleted?: '' + region?: string + } + +// Ref: https://fly.io/docs/machines/working-with-machines/#create-a-machine +export interface CreateMachineRequest extends ApiCreateMachineRequest { + app_name: string + config: MachineConfig +} + +interface BaseEvent { + id: string + type: string + status: string + source: 'flyd' | 'user' + timestamp: number +} + +interface StartEvent extends BaseEvent { + type: 'start' + status: 'started' | 'starting' +} + +interface LaunchEvent extends BaseEvent { + type: 'launch' + status: 'created' + source: 'user' +} + +interface RestartEvent extends BaseEvent { + type: 'restart' + status: 'starting' | 'stopping' + source: 'flyd' | 'user' +} + +interface ExitEvent extends BaseEvent { + type: 'exit' + status: 'stopped' + source: 'flyd' + request: { + exit_event: { + requested_stop: boolean + restarting: boolean + guest_exit_code: number + guest_signal: number + guest_error: string + exit_code: number + signal: number + error: string + oom_killed: boolean + exited_at: string + } + restart_count: number + } +} + +export type MachineEvent = LaunchEvent | StartEvent | RestartEvent | ExitEvent + +export enum MachineState { + Created = 'created', + Starting = 'starting', + Started = 'started', + Stopping = 'stopping', + Stopped = 'stopped', + Replacing = 'replacing', + Destroying = 'destroying', + Destroyed = 'destroyed', +} + +interface MachineMount extends ApiMachineMount { + encrypted: boolean + // Absolute path on the VM where the volume should be mounted. i.e. /data + path: string + size_gb: number + // The volume ID, visible in fly volumes list, i.e. vol_2n0l3vl60qpv635d + volume: string + name: string +} export enum ConnectionHandler { // Convert TLS connection to unencrypted TCP @@ -11,9 +118,17 @@ export enum ConnectionHandler { PROXY_PROTO = 'proxy_proto', } -interface ServiceConfig { - // tcp or udp. Learn more about running raw TCP/UDP services. +interface MachinePort extends ApiMachinePort { + // Public-facing port number + port: number + // Array of connection handlers for TCP-based services. + handlers?: ConnectionHandler[] +} + +interface MachineService extends ApiMachineService { protocol: 'tcp' | 'udp' + internal_port: number + ports: MachinePort[] // load balancing concurrency settings concurrency?: { // connections (TCP) or requests (HTTP). Defaults to connections. @@ -23,18 +138,9 @@ interface ServiceConfig { // maximum allowed concurrency. We will queue or reject when a service is at this limit hard_limit: number } - // Port the machine VM listens on - internal_port: number - // An array of objects defining the service's ports and associated handlers. Options: - ports: { - // Public-facing port number - port: number - // Array of connection handlers for TCP-based services. - handlers?: ConnectionHandler[] - }[] } -interface HealthCheckConfig { +interface MachineCheck extends ApiMachineCheck { // tcp or http type: 'tcp' | 'http' // The port to connect to, likely should be the same as internal_port @@ -43,104 +149,30 @@ interface HealthCheckConfig { interval: string // The maximum time a connection can take before being reported as failing its healthcheck timeout: string - // For http checks, the HTTP method to use to when making the request - method?: string - // For http checks, the path to send the request to - path?: string -} - -export interface MachineConfig { - // The Docker image to run - image: string - // An object with the following options: - guest?: { - // Number of vCPUs (default 1) - cpus?: number - // Memory in megabytes as multiples of 256 (default 256) - memory_mb?: number - // Optional array of strings. Arguments passed to the kernel - kernel_args?: string[] - } - // A named size for the VM, e.g. performance-2x or shared-cpu-2x. Note: guest and size are mutually exclusive. - size?: string - // An object filled with key/value pairs to be set as environment variables - env?: Record - // An array of objects that define a single network service. Check the machines networking section for more information. Options: - services: ServiceConfig[] - // An optional array of objects defining multiple processes to run within a VM. The Machine will stop if any process exits without error. - processes?: { - // Process name - name: string - // An array of strings. The process that will run - entrypoint: string[] - // An array of strings. The arguments passed to the entrypoint - cmd: string[] - // An object filled with key/value pairs to be set as environment variables - env: Record - // An optional user that the process runs under - user?: string - }[] - // Optionally one of hourly, daily, weekly, monthly. Runs machine at the given interval. Interval starts at time of machine creation - schedule?: 'hourly' | 'daily' | 'weekly' | 'monthly' - // An array of objects that reference previously created persistent volumes. Currently, you may only mount one volume per VM. - mounts?: { - // The volume ID, visible in fly volumes list, i.e. vol_2n0l3vl60qpv635d - volume: string - // Absolute path on the VM where the volume should be mounted. i.e. /data - path: string - }[] - // An optional object that defines one or more named checks - // the key for each check is the check name - // the value for each check supports: - checks?: Record - metadata?: Record -} - -export type ListMachineRequest = string - -// Ref: https://fly.io/docs/machines/working-with-machines/#create-a-machine -export interface CreateMachineRequest { - appId: string - // Unique name for this machine. If omitted, one is generated for you. - name?: string - // The target region. Omitting this param launches in the same region as your WireGuard peer connection (somewhere near you). - region?: string - // An object defining the machine configuration. Options - config: MachineConfig } -interface BaseEvent { - id: string - type: string - status: string - source: 'flyd' | 'user' - timestamp: number -} - -interface StartEvent extends BaseEvent { - type: 'start' - status: 'started' - source: 'flyd' +interface MachineGuest extends ApiMachineGuest { + cpu_kind: 'shared' | 'performance' + cpus: number + memory_mb: number } -interface LaunchEvent extends BaseEvent { - type: 'launch' - status: 'created' - source: 'user' +interface CheckStatus extends ApiCheckStatus { + name: string + status: 'passing' | 'warning' | 'critical' + output: string + updated_at: string } -export enum MachineState { - Created = 'created', - Starting = 'starting', - Started = 'started', - Stopping = 'stopping', - Stopped = 'stopped', - Replacing = 'replacing', - Destroying = 'destroying', - Destroyed = 'destroyed', +interface MachineImageRef extends Omit { + registry: string + repository: string + tag: string + digest: string + labels: Record | null } -export interface MachineResponse { +export interface MachineResponse extends Omit { id: string name: string state: MachineState @@ -149,58 +181,24 @@ export interface MachineResponse { private_ip: string config: { env: Record - init: { - cmd?: string[] - entrypoint?: string[] - exec?: string[] - swap_size_mb?: number - tty?: boolean - } - mounts: { - encrypted: boolean - path: string - size_gb: number - volume: string - name: string - }[] - services: ServiceConfig[] - checks: Record - image: string - restart: { - max_retries?: number - policy?: string - } - guest: { - cpu_kind: 'shared' - cpus: number - memory_mb: number - } + init: ApiMachineInit + mounts: MachineMount[] + services: MachineService[] + checks: Record + restart: ApiMachineRestart + guest: MachineGuest size: 'shared-cpu-1x' | 'shared-cpu-2x' | 'shared-cpu-4x' - } - image_ref: { - registry: string - repository: string - tag: string - digest: string - labels: Record | null - } + } & MachineConfig + image_ref: MachineImageRef created_at: string updated_at: string - events: (StartEvent | LaunchEvent)[] - checks: { - name: string - status: 'passing' | 'warning' | 'critical' - output: string - updated_at: string - }[] + events: MachineEvent[] + checks: CheckStatus[] } -export const FLY_API_HOSTNAME = - process.env.FLY_API_HOSTNAME || 'https://api.machines.dev' - export interface GetMachineRequest { - appId: string - machineId: string + app_name: string + machine_id: string } interface OkResponse { @@ -212,29 +210,86 @@ export interface DeleteMachineRequest extends GetMachineRequest { force?: boolean } -export interface StopMachineRequest extends GetMachineRequest { - signal?: - | 'SIGABRT' - | 'SIGALRM' - | 'SIGFPE' - | 'SIGILL' - | 'SIGINT' - | 'SIGKILL' - | 'SIGPIPE' - | 'SIGQUIT' - | 'SIGSEGV' - | 'SIGTERM' - | 'SIGTRAP' - | 'SIGUSR1' +export interface RestartMachineRequest extends GetMachineRequest { timeout?: string } +export interface SignalMachineRequest extends GetMachineRequest { + signal: ApiMachineSignal +} + +export interface StopMachineRequest extends RestartMachineRequest { + signal?: ApiMachineSignal +} + export type StartMachineRequest = GetMachineRequest export interface UpdateMachineRequest extends GetMachineRequest { config: MachineConfig } +export type ListEventsRequest = GetMachineRequest + +export type ListVersionsRequest = GetMachineRequest + +export interface ListProcessesRequest extends GetMachineRequest { + sort_by?: string + order?: string +} + +export interface ProcessResponse { + command: string + cpu: number + directory: string + listen_sockets: [ + { + address: string + proto: string + } + ] + pid: number + rss: number + rtime: number + stime: number +} + +export interface WaitMachineRequest extends GetMachineRequest { + instance_id?: string + // Default timeout is 60s + timeout?: string + state?: ApiMachineState +} + +export interface MachineVersionResponse { + user_config: MachineResponse + version: string +} + +export type GetLeaseRequest = GetMachineRequest + +export interface LeaseResponse { + status: 'success' + data: { + description: string + expires_at: number + nonce: string + owner: string + } +} + +export interface AcquireLeaseRequest extends GetLeaseRequest { + description?: string + ttl: number +} + +export interface DeleteLeaseRequest extends GetLeaseRequest { + nonce: string +} + +export type CordonMachineRequest = GetMachineRequest + +export type UncordonMachineRequest = GetMachineRequest + export class Machine { private client: Client @@ -242,48 +297,124 @@ export class Machine { this.client = client } - async listMachines(appId: ListMachineRequest): Promise { - const path = `apps/${appId}/machines` + async listMachines(app_name: ListMachineRequest): Promise { + let path: string + if (typeof app_name === 'string') { + path = `apps/${app_name}/machines` + } else { + const { app_name: appId, ...params } = app_name + path = `apps/${appId}/machines` + const query = new URLSearchParams(params).toString() + if (query) path += `?${query}` + } return await this.client.restOrThrow(path) } - async getMachine(payload: StartMachineRequest): Promise { - const { appId, machineId } = payload - const path = `apps/${appId}/machines/${machineId}` + async getMachine(payload: GetMachineRequest): Promise { + const { app_name, machine_id } = payload + const path = `apps/${app_name}/machines/${machine_id}` return await this.client.restOrThrow(path) } + async createMachine(payload: CreateMachineRequest): Promise { + const { app_name, ...body } = payload + const path = `apps/${app_name}/machines` + return await this.client.restOrThrow(path, 'POST', body) + } + + async updateMachine(payload: UpdateMachineRequest): Promise { + const { app_name, machine_id, ...body } = payload + const path = `apps/${app_name}/machines/${machine_id}` + return await this.client.restOrThrow(path, 'POST', body) + } + + async deleteMachine(payload: DeleteMachineRequest): Promise { + const { app_name, machine_id, force } = payload + const query = force ? '?kill=true' : '' + const path = `apps/${app_name}/machines/${machine_id}${query}` + return await this.client.restOrThrow(path, 'DELETE') + } + async startMachine(payload: StartMachineRequest): Promise { - const { appId, machineId } = payload - const path = `apps/${appId}/machines/${machineId}/start` + const { app_name, machine_id } = payload + const path = `apps/${app_name}/machines/${machine_id}/start` return await this.client.restOrThrow(path, 'POST') } async stopMachine(payload: StopMachineRequest): Promise { - const { appId, machineId, ...body } = payload - const path = `apps/${appId}/machines/${machineId}/stop` + const { app_name, machine_id, ...body } = payload + const path = `apps/${app_name}/machines/${machine_id}/stop` return await this.client.restOrThrow(path, 'POST', { - signal: 'SIGTERM', + signal: ApiMachineSignal.SIGTERM, ...body, }) } - async deleteMachine(payload: DeleteMachineRequest): Promise { - const { appId, machineId, force } = payload - const query = force ? '?kill=true' : '' - const path = `apps/${appId}/machines/${machineId}${query}` - return await this.client.restOrThrow(path, 'DELETE') + async restartMachine(payload: RestartMachineRequest): Promise { + const { app_name, machine_id, ...body } = payload + const path = `apps/${app_name}/machines/${machine_id}/restart` + return await this.client.restOrThrow(path, 'POST', body) } - async createMachine(payload: CreateMachineRequest): Promise { - const { appId, ...body } = payload - const path = `apps/${appId}/machines` + async signalMachine(payload: SignalMachineRequest): Promise { + const { app_name, machine_id, ...body } = payload + const path = `apps/${app_name}/machines/${machine_id}/signal` return await this.client.restOrThrow(path, 'POST', body) } - async updateMachine(payload: UpdateMachineRequest): Promise { - const { appId, machineId, ...body } = payload - const path = `apps/${appId}/machines/${machineId}` + async listEvents( + payload: ListEventsRequest + ): Promise { + const { app_name, machine_id } = payload + const path = `apps/${app_name}/machines/${machine_id}/events` + return await this.client.restOrThrow(path) + } + + async listVersions( + payload: ListVersionsRequest + ): Promise { + const { app_name, machine_id } = payload + const path = `apps/${app_name}/machines/${machine_id}/versions` + return await this.client.restOrThrow(path) + } + + async listProcesses(payload: ListProcessesRequest): Promise { + const { app_name, machine_id, ...params } = payload + let path = `apps/${app_name}/machines/${machine_id}/ps` + const query = new URLSearchParams(params).toString() + if (query) path += `?${query}` + return await this.client.restOrThrow(path) + } + + async waitMachine(payload: WaitMachineRequest): Promise { + const { app_name, machine_id, ...params } = payload + let path = `apps/${app_name}/machines/${machine_id}/wait` + const query = new URLSearchParams(params).toString() + if (query) path += `?${query}` + return await this.client.restOrThrow(path) + } + + async getLease(payload: GetLeaseRequest): Promise { + const { app_name, machine_id } = payload + const path = `apps/${app_name}/machines/${machine_id}/lease` + return await this.client.restOrThrow(path) + } + + async acquireLease(payload: AcquireLeaseRequest): Promise { + const { app_name, machine_id, ...body } = payload + const path = `apps/${app_name}/machines/${machine_id}/lease` return await this.client.restOrThrow(path, 'POST', body) } + + async cordonMachine(payload: CordonMachineRequest): Promise { + const { app_name, machine_id } = payload + const path = `apps/${app_name}/machines/${machine_id}/cordon` + return await this.client.restOrThrow(path, 'POST') + } + + async uncordonMachine(payload: UncordonMachineRequest): Promise { + const { app_name, machine_id } = payload + const path = `apps/${app_name}/machines/${machine_id}/uncordon` + return await this.client.restOrThrow(path, 'POST') + } } diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..08dcbcc --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,460 @@ +/* eslint-disable */ +/* tslint:disable */ +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +export interface App { + name?: string + organization?: Organization + status?: string +} + +export interface CheckStatus { + name?: string + output?: string + status?: string + updated_at?: string +} + +export interface CreateAppRequest { + app_name?: string + network?: string + org_slug?: string +} + +export interface CreateLeaseRequest { + description?: string + ttl?: number +} + +export interface CreateMachineRequest { + config?: ApiMachineConfig + lease_ttl?: number + name?: string + region?: string + skip_launch?: boolean + skip_service_registration?: boolean +} + +export interface CreateVolumeRequest { + compute?: ApiMachineGuest + encrypted?: boolean + fstype?: string + machines_only?: boolean + name?: string + region?: string + require_unique_zone?: boolean + size_gb?: number + /** restore from snapshot */ + snapshot_id?: string + /** fork from remote volume */ + source_volume_id?: string +} + +export interface ErrorResponse { + error?: string +} + +export interface ExtendVolumeRequest { + size_gb?: number +} + +export interface ExtendVolumeResponse { + needs_restart?: boolean + volume?: Volume +} + +export interface ImageRef { + digest?: string + labels?: Record + registry?: string + repository?: string + tag?: string +} + +export interface Lease { + /** Description or reason for the Lease. */ + description?: string + /** ExpiresAt is the unix timestamp in UTC to denote when the Lease will no longer be valid. */ + expires_at?: number + /** Nonce is the unique ID autogenerated and associated with the Lease. */ + nonce?: string + /** Owner is the user identifier which acquired the Lease. */ + owner?: string +} + +export interface ListApp { + machine_count?: number + name?: string + network?: any +} + +export interface ListAppsResponse { + apps?: ListApp[] + total_apps?: number +} + +export interface ListenSocket { + address?: string + proto?: string +} + +export interface Machine { + checks?: CheckStatus[] + config?: ApiMachineConfig + created_at?: string + events?: MachineEvent[] + id?: string + image_ref?: ImageRef + /** InstanceID is unique for each version of the machine */ + instance_id?: string + name?: string + /** Nonce is only every returned on machine creation if a lease_duration was provided. */ + nonce?: string + /** PrivateIP is the internal 6PN address of the machine. */ + private_ip?: string + region?: string + state?: string + updated_at?: string +} + +export interface MachineEvent { + id?: string + request?: any + source?: string + status?: string + timestamp?: number + type?: string +} + +export interface MachineExecRequest { + /** Deprecated: use Command instead */ + cmd?: string + command?: string[] + timeout?: number +} + +export interface MachineVersion { + user_config?: ApiMachineConfig + version?: string +} + +export interface Organization { + name?: string + slug?: string +} + +export interface ProcessStat { + command?: string + cpu?: number + directory?: string + listen_sockets?: ListenSocket[] + pid?: number + rss?: number + rtime?: number + stime?: number +} + +export interface SignalRequest { + signal?: SignalRequestSignalEnum +} + +export interface StopRequest { + signal?: string + timeout?: string +} + +export interface UpdateMachineRequest { + config?: ApiMachineConfig + current_version?: string + lease_ttl?: number + name?: string + region?: string + skip_launch?: boolean + skip_service_registration?: boolean +} + +export interface Volume { + attached_alloc_id?: string + attached_machine_id?: string + block_size?: number + blocks?: number + blocks_avail?: number + blocks_free?: number + created_at?: string + encrypted?: boolean + fstype?: string + id?: string + name?: string + region?: string + size_gb?: number + state?: string + zone?: string +} + +export interface VolumeSnapshot { + created_at?: string + digest?: string + id?: string + size?: number +} + +export interface ApiDNSConfig { + skip_registration?: boolean +} + +export interface ApiFile { + /** + * GuestPath is the path on the machine where the file will be written and must be an absolute path. + * i.e. /full/path/to/file.json + */ + guest_path?: string + /** RawValue containts the base64 encoded string of the file contents. */ + raw_value?: string + /** SecretName is the name of the secret that contains the base64 encoded file contents. */ + secret_name?: string +} + +export interface ApiHTTPOptions { + compress?: boolean + response?: ApiHTTPResponseOptions +} + +export interface ApiHTTPResponseOptions { + headers?: Record +} + +export interface ApiMachineCheck { + grace_period?: string + headers?: ApiMachineHTTPHeader[] + interval?: string + method?: string + path?: string + port?: number + protocol?: string + timeout?: string + tls_server_name?: string + tls_skip_verify?: boolean + type?: string +} + +export interface ApiMachineConfig { + auto_destroy?: boolean + checks?: Record + /** Deprecated: use Service.Autostart instead */ + disable_machine_autostart?: boolean + dns?: ApiDNSConfig + /** + * Fields managed from fly.toml + * If you add anything here, ensure appconfig.Config.ToMachine() is updated + */ + env?: Record + files?: ApiFile[] + guest?: ApiMachineGuest + host_dedication_id?: string + /** Set by fly deploy or fly machines commands */ + image?: string + init?: ApiMachineInit + metadata?: Record + metrics?: ApiMachineMetrics + mounts?: ApiMachineMount[] + processes?: ApiMachineProcess[] + restart?: ApiMachineRestart + /** + * The following fields can only be set or updated by `fly machines run|update` commands + * "fly deploy" must preserve them, if you add anything here, ensure it is propagated on deploys + */ + schedule?: string + services?: ApiMachineService[] + /** Deprecated: use Guest instead */ + size?: string + /** + * Standbys enable a machine to be a standby for another. In the event of a hardware failure, + * the standby machine will be started. + */ + standbys?: string[] + statics?: ApiStatic[] + stop_config?: ApiStopConfig +} + +export interface ApiMachineGuest { + cpu_kind?: string + cpus?: number + gpus?: number + kernel_args?: string[] + memory_mb?: number +} + +export interface ApiMachineHTTPHeader { + name?: string + values?: string[] +} + +export interface ApiMachineInit { + cmd?: string[] + entrypoint?: string[] + exec?: string[] + kernel_args?: string[] + swap_size_mb?: number + tty?: boolean +} + +export interface ApiMachineMetrics { + path?: string + port?: number +} + +export interface ApiMachineMount { + encrypted?: boolean + name?: string + path?: string + size_gb?: number + volume?: string +} + +export interface ApiMachinePort { + end_port?: number + force_https?: boolean + handlers?: string[] + http_options?: ApiHTTPOptions + port?: number + proxy_proto_options?: ApiProxyProtoOptions + start_port?: number + tls_options?: ApiTLSOptions +} + +export interface ApiMachineProcess { + cmd?: string[] + entrypoint?: string[] + env?: Record + exec?: string[] + user?: string +} + +export interface ApiMachineRestart { + /** MaxRetries is only relevant with the on-failure policy. */ + max_retries?: number + policy?: string +} + +export interface ApiMachineService { + autostart?: boolean + autostop?: boolean + checks?: ApiMachineCheck[] + concurrency?: ApiMachineServiceConcurrency + force_instance_description?: string + force_instance_key?: string + internal_port?: number + min_machines_running?: number + ports?: ApiMachinePort[] + protocol?: string +} + +export interface ApiMachineServiceConcurrency { + hard_limit?: number + soft_limit?: number + type?: string +} + +export interface ApiProxyProtoOptions { + version?: string +} + +export interface ApiStatic { + guest_path: string + url_prefix: string +} + +export interface ApiStopConfig { + signal?: string + timeout?: string +} + +export interface ApiTLSOptions { + alpn?: string[] + default_self_signed?: boolean + versions?: string[] +} + +export enum SignalRequestSignalEnum { + SIGABRT = 'SIGABRT', + SIGALRM = 'SIGALRM', + SIGFPE = 'SIGFPE', + SIGHUP = 'SIGHUP', + SIGILL = 'SIGILL', + SIGINT = 'SIGINT', + SIGKILL = 'SIGKILL', + SIGPIPE = 'SIGPIPE', + SIGQUIT = 'SIGQUIT', + SIGSEGV = 'SIGSEGV', + SIGTERM = 'SIGTERM', + SIGTRAP = 'SIGTRAP', + SIGUSR1 = 'SIGUSR1', +} + +export interface AppsListParams { + /** The org slug, or 'personal', to filter apps */ + org_slug?: string +} + +export interface MachinesListParams { + /** Include deleted machines */ + include_deleted?: boolean + /** Region filter */ + region?: string + /** Fly App Name */ + appName: string +} + +export interface MachinesListProcessesParams { + /** Sort by */ + sort_by?: string + /** Order */ + order?: string + /** Fly App Name */ + appName: string + /** Machine ID */ + machineId: string +} + +export interface MachinesRestartParams { + /** Restart timeout as a Go duration string or number of seconds */ + timeout?: string + /** Fly App Name */ + appName: string + /** Machine ID */ + machineId: string +} + +export interface MachinesWaitParams { + /** instance? version? TODO */ + instance_id?: string + /** wait timeout. default 60s */ + timeout?: number + /** desired state */ + state?: StateEnum + /** Fly App Name */ + appName: string + /** Machine ID */ + machineId: string +} + +/** desired state */ +export enum StateEnum { + Started = 'started', + Stopped = 'stopped', + Destroyed = 'destroyed', +} + +/** desired state */ +export enum MachinesWaitParams1StateEnum { + Started = 'started', + Stopped = 'stopped', + Destroyed = 'destroyed', +} diff --git a/src/lib/volume.ts b/src/lib/volume.ts index 93c9c8d..4b449f5 100644 --- a/src/lib/volume.ts +++ b/src/lib/volume.ts @@ -1,20 +1,13 @@ import Client from '../client' +import { CreateVolumeRequest as ApiCreateVolumeRequest } from './types' export type ListVolumesRequest = string // Ref: https://github.com/superfly/flyctl/blob/master/api/volume_types.go#L23 -export interface CreateVolumeRequest { - appId: string +export interface CreateVolumeRequest extends ApiCreateVolumeRequest { + app_name: string name: string region: string - size_gb?: number - encrypted?: boolean - require_unique_zone?: boolean - machines_only?: boolean - // restore from snapshot - snapshot_id?: string - // fork from remote volume - source_volume_id?: string } // Ref: https://github.com/superfly/flyctl/blob/master/api/volume_types.go#L5 @@ -38,8 +31,8 @@ export interface VolumeResponse { } export interface GetVolumeRequest { - appId: string - volumeId: string + app_name: string + volume_id: string } export type DeleteVolumeRequest = GetVolumeRequest @@ -53,6 +46,15 @@ export interface ExtendVolumeResponse { volume: VolumeResponse } +export type ListSnapshotsRequest = GetVolumeRequest + +export interface SnapshotResponse { + id: string + created_at: string + digest: string + size: number +} + export class Volume { private client: Client @@ -60,34 +62,42 @@ export class Volume { this.client = client } - async listVolumes(appId: ListVolumesRequest): Promise { - const path = `apps/${appId}/volumes` + async listVolumes(app_name: ListVolumesRequest): Promise { + const path = `apps/${app_name}/volumes` return await this.client.restOrThrow(path) } async getVolume(payload: GetVolumeRequest): Promise { - const { appId, volumeId } = payload - const path = `apps/${appId}/volumes/${volumeId}` + const { app_name, volume_id } = payload + const path = `apps/${app_name}/volumes/${volume_id}` return await this.client.restOrThrow(path) } async createVolume(payload: CreateVolumeRequest): Promise { - const { appId, ...body } = payload - const path = `apps/${appId}/volumes` + const { app_name, ...body } = payload + const path = `apps/${app_name}/volumes` return await this.client.restOrThrow(path, 'POST', body) } async deleteVolume(payload: DeleteVolumeRequest): Promise { - const { appId, volumeId } = payload - const path = `apps/${appId}/volumes/${volumeId}` + const { app_name, volume_id } = payload + const path = `apps/${app_name}/volumes/${volume_id}` return await this.client.restOrThrow(path, 'DELETE') } async extendVolume( payload: ExtendVolumeRequest ): Promise { - const { appId, volumeId, ...body } = payload - const path = `apps/${appId}/volumes/${volumeId}/extend` + const { app_name, volume_id, ...body } = payload + const path = `apps/${app_name}/volumes/${volume_id}/extend` return await this.client.restOrThrow(path, 'PUT', body) } + + async listSnapshots( + payload: ListSnapshotsRequest + ): Promise { + const { app_name, volume_id } = payload + const path = `apps/${app_name}/volumes/${volume_id}/snapshots` + return await this.client.restOrThrow(path) + } }