@vibesdotdev/infra-doks 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/SPEC.md +285 -0
- package/dist/client/digitalocean-app-deploy.client.d.ts +46 -0
- package/dist/client/digitalocean-app-deploy.client.d.ts.map +1 -0
- package/dist/client/digitalocean-app-deploy.client.js +135 -0
- package/dist/client/digitalocean-app-deploy.client.js.map +1 -0
- package/dist/client/index.d.ts +15 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/index.js.map +1 -0
- package/dist/cloud/base.d.ts +33 -0
- package/dist/cloud/base.d.ts.map +1 -0
- package/dist/cloud/base.js +86 -0
- package/dist/cloud/base.js.map +1 -0
- package/dist/cloud/digitalocean.d.ts +33 -0
- package/dist/cloud/digitalocean.d.ts.map +1 -0
- package/dist/cloud/digitalocean.js +258 -0
- package/dist/cloud/digitalocean.js.map +1 -0
- package/dist/cloud/factory.d.ts +28 -0
- package/dist/cloud/factory.d.ts.map +1 -0
- package/dist/cloud/factory.js +151 -0
- package/dist/cloud/factory.js.map +1 -0
- package/dist/cloud/index.d.ts +12 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +11 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/doks.plugin.d.ts +41 -0
- package/dist/doks.plugin.d.ts.map +1 -0
- package/dist/doks.plugin.js +287 -0
- package/dist/doks.plugin.js.map +1 -0
- package/dist/implementations/deployment.impl.d.ts +34 -0
- package/dist/implementations/deployment.impl.d.ts.map +1 -0
- package/dist/implementations/deployment.impl.js +86 -0
- package/dist/implementations/deployment.impl.js.map +1 -0
- package/dist/implementations/droplet.impl.d.ts +85 -0
- package/dist/implementations/droplet.impl.d.ts.map +1 -0
- package/dist/implementations/droplet.impl.js +113 -0
- package/dist/implementations/droplet.impl.js.map +1 -0
- package/dist/implementations/gitea.impl.d.ts +68 -0
- package/dist/implementations/gitea.impl.d.ts.map +1 -0
- package/dist/implementations/gitea.impl.js +295 -0
- package/dist/implementations/gitea.impl.js.map +1 -0
- package/dist/implementations/managed-db.impl.d.ts +25 -0
- package/dist/implementations/managed-db.impl.d.ts.map +1 -0
- package/dist/implementations/managed-db.impl.js +31 -0
- package/dist/implementations/managed-db.impl.js.map +1 -0
- package/dist/implementations/managed-redis.impl.d.ts +37 -0
- package/dist/implementations/managed-redis.impl.d.ts.map +1 -0
- package/dist/implementations/managed-redis.impl.js +40 -0
- package/dist/implementations/managed-redis.impl.js.map +1 -0
- package/dist/implementations/spaces.impl.d.ts +36 -0
- package/dist/implementations/spaces.impl.d.ts.map +1 -0
- package/dist/implementations/spaces.impl.js +40 -0
- package/dist/implementations/spaces.impl.js.map +1 -0
- package/dist/implementations/statefulset.impl.d.ts +65 -0
- package/dist/implementations/statefulset.impl.d.ts.map +1 -0
- package/dist/implementations/statefulset.impl.js +165 -0
- package/dist/implementations/statefulset.impl.js.map +1 -0
- package/dist/implementations/verdaccio.impl.d.ts +65 -0
- package/dist/implementations/verdaccio.impl.d.ts.map +1 -0
- package/dist/implementations/verdaccio.impl.js +259 -0
- package/dist/implementations/verdaccio.impl.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/kubernetes/index.d.ts +95 -0
- package/dist/kubernetes/index.d.ts.map +1 -0
- package/dist/kubernetes/index.js +625 -0
- package/dist/kubernetes/index.js.map +1 -0
- package/dist/secrets/index.d.ts +4 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +4 -0
- package/dist/secrets/index.js.map +1 -0
- package/dist/secrets/vault.descriptor.d.ts +10 -0
- package/dist/secrets/vault.descriptor.d.ts.map +1 -0
- package/dist/secrets/vault.descriptor.js +25 -0
- package/dist/secrets/vault.descriptor.js.map +1 -0
- package/dist/secrets/vault.impl.cloud.d.ts +40 -0
- package/dist/secrets/vault.impl.cloud.d.ts.map +1 -0
- package/dist/secrets/vault.impl.cloud.js +178 -0
- package/dist/secrets/vault.impl.cloud.js.map +1 -0
- package/dist/secrets/vault.impl.d.ts +29 -0
- package/dist/secrets/vault.impl.d.ts.map +1 -0
- package/dist/secrets/vault.impl.js +137 -0
- package/dist/secrets/vault.impl.js.map +1 -0
- package/dist/types.d.ts +509 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +47 -0
- package/dist/types.js.map +1 -0
- package/package.json +145 -0
- package/src/client/digitalocean-app-deploy.client.ts +226 -0
- package/src/client/index.ts +24 -0
- package/src/cloud/base.ts +149 -0
- package/src/cloud/digitalocean.ts +363 -0
- package/src/cloud/factory.ts +190 -0
- package/src/cloud/index.ts +81 -0
- package/src/doks.plugin.ts +401 -0
- package/src/implementations/deployment.impl.ts +93 -0
- package/src/implementations/droplet.impl.ts +157 -0
- package/src/implementations/gitea.impl.ts +319 -0
- package/src/implementations/managed-db.impl.ts +37 -0
- package/src/implementations/managed-redis.impl.ts +49 -0
- package/src/implementations/spaces.impl.ts +52 -0
- package/src/implementations/statefulset.impl.ts +186 -0
- package/src/implementations/verdaccio.impl.ts +300 -0
- package/src/index.ts +136 -0
- package/src/kubernetes/index.ts +754 -0
- package/src/secrets/index.ts +9 -0
- package/src/secrets/vault.descriptor.ts +28 -0
- package/src/secrets/vault.impl.cloud.ts +278 -0
- package/src/secrets/vault.impl.ts +149 -0
- package/src/types.ts +563 -0
package/package.json
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibesdotdev/infra-doks",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"bun": "./src/index.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./plugin": {
|
|
15
|
+
"types": "./dist/doks.plugin.d.ts",
|
|
16
|
+
"bun": "./src/doks.plugin.ts",
|
|
17
|
+
"import": "./dist/doks.plugin.js",
|
|
18
|
+
"default": "./dist/doks.plugin.js"
|
|
19
|
+
},
|
|
20
|
+
"./implementations/deployment": {
|
|
21
|
+
"types": "./dist/implementations/deployment.impl.d.ts",
|
|
22
|
+
"bun": "./src/implementations/deployment.impl.ts",
|
|
23
|
+
"import": "./dist/implementations/deployment.impl.js",
|
|
24
|
+
"default": "./dist/implementations/deployment.impl.js"
|
|
25
|
+
},
|
|
26
|
+
"./implementations/statefulset": {
|
|
27
|
+
"types": "./dist/implementations/statefulset.impl.d.ts",
|
|
28
|
+
"bun": "./src/implementations/statefulset.impl.ts",
|
|
29
|
+
"import": "./dist/implementations/statefulset.impl.js",
|
|
30
|
+
"default": "./dist/implementations/statefulset.impl.js"
|
|
31
|
+
},
|
|
32
|
+
"./implementations/managed-db": {
|
|
33
|
+
"types": "./dist/implementations/managed-db.impl.d.ts",
|
|
34
|
+
"bun": "./src/implementations/managed-db.impl.ts",
|
|
35
|
+
"import": "./dist/implementations/managed-db.impl.js",
|
|
36
|
+
"default": "./dist/implementations/managed-db.impl.js"
|
|
37
|
+
},
|
|
38
|
+
"./implementations/managed-redis": {
|
|
39
|
+
"types": "./dist/implementations/managed-redis.impl.d.ts",
|
|
40
|
+
"bun": "./src/implementations/managed-redis.impl.ts",
|
|
41
|
+
"import": "./dist/implementations/managed-redis.impl.js",
|
|
42
|
+
"default": "./dist/implementations/managed-redis.impl.js"
|
|
43
|
+
},
|
|
44
|
+
"./implementations/spaces": {
|
|
45
|
+
"types": "./dist/implementations/spaces.impl.d.ts",
|
|
46
|
+
"bun": "./src/implementations/spaces.impl.ts",
|
|
47
|
+
"import": "./dist/implementations/spaces.impl.js",
|
|
48
|
+
"default": "./dist/implementations/spaces.impl.js"
|
|
49
|
+
},
|
|
50
|
+
"./implementations/gitea": {
|
|
51
|
+
"types": "./dist/implementations/gitea.impl.d.ts",
|
|
52
|
+
"bun": "./src/implementations/gitea.impl.ts",
|
|
53
|
+
"import": "./dist/implementations/gitea.impl.js",
|
|
54
|
+
"default": "./dist/implementations/gitea.impl.js"
|
|
55
|
+
},
|
|
56
|
+
"./implementations/verdaccio": {
|
|
57
|
+
"types": "./dist/implementations/verdaccio.impl.d.ts",
|
|
58
|
+
"bun": "./src/implementations/verdaccio.impl.ts",
|
|
59
|
+
"import": "./dist/implementations/verdaccio.impl.js",
|
|
60
|
+
"default": "./dist/implementations/verdaccio.impl.js"
|
|
61
|
+
},
|
|
62
|
+
"./cloud": {
|
|
63
|
+
"types": "./dist/cloud/index.d.ts",
|
|
64
|
+
"bun": "./src/cloud/index.ts",
|
|
65
|
+
"import": "./dist/cloud/index.js",
|
|
66
|
+
"default": "./dist/cloud/index.js"
|
|
67
|
+
},
|
|
68
|
+
"./kubernetes": {
|
|
69
|
+
"types": "./dist/kubernetes/index.d.ts",
|
|
70
|
+
"bun": "./src/kubernetes/index.ts",
|
|
71
|
+
"import": "./dist/kubernetes/index.js",
|
|
72
|
+
"default": "./dist/kubernetes/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./secrets": {
|
|
75
|
+
"types": "./dist/secrets/index.d.ts",
|
|
76
|
+
"bun": "./src/secrets/index.ts",
|
|
77
|
+
"import": "./dist/secrets/index.js",
|
|
78
|
+
"default": "./dist/secrets/index.js"
|
|
79
|
+
},
|
|
80
|
+
"./types": {
|
|
81
|
+
"types": "./dist/types.d.ts",
|
|
82
|
+
"bun": "./src/types.ts",
|
|
83
|
+
"import": "./dist/types.js",
|
|
84
|
+
"default": "./dist/types.js"
|
|
85
|
+
},
|
|
86
|
+
"./client": {
|
|
87
|
+
"types": "./dist/client/index.d.ts",
|
|
88
|
+
"bun": "./src/client/index.ts",
|
|
89
|
+
"import": "./dist/client/index.js",
|
|
90
|
+
"default": "./dist/client/index.js"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"dependencies": {
|
|
94
|
+
"@kubernetes/client-node": "^1.4.0",
|
|
95
|
+
"@vibesdotdev/infra-core": "0.0.1",
|
|
96
|
+
"@vibesdotdev/logging": "0.0.1",
|
|
97
|
+
"@vibesdotdev/registry-theme": "0.0.1",
|
|
98
|
+
"@vibesdotdev/runtime": "0.0.1",
|
|
99
|
+
"@vibesdotdev/runtime-client": "0.0.1",
|
|
100
|
+
"@vibesdotdev/secrets": "0.0.1",
|
|
101
|
+
"zod": "^4.3.6"
|
|
102
|
+
},
|
|
103
|
+
"scripts": {
|
|
104
|
+
"check": "bun --bun tsc -p tsconfig.json --noEmit",
|
|
105
|
+
"test": "bun test src/**/*.test.ts",
|
|
106
|
+
"build": "tsc -p tsconfig.json"
|
|
107
|
+
},
|
|
108
|
+
"publishConfig": {
|
|
109
|
+
"registry": "https://registry.npmjs.org",
|
|
110
|
+
"access": "public"
|
|
111
|
+
},
|
|
112
|
+
"repository": {
|
|
113
|
+
"type": "git",
|
|
114
|
+
"url": "git+https://github.com/vibesdotdev/monorepo.git",
|
|
115
|
+
"directory": "packages/infra-doks"
|
|
116
|
+
},
|
|
117
|
+
"license": "MIT",
|
|
118
|
+
"files": [
|
|
119
|
+
"dist",
|
|
120
|
+
"src",
|
|
121
|
+
"bin",
|
|
122
|
+
"README.md",
|
|
123
|
+
"SPEC.md",
|
|
124
|
+
"LICENSE",
|
|
125
|
+
"!src/**/__tests__/**",
|
|
126
|
+
"!src/**/__stubs__/**",
|
|
127
|
+
"!src/**/*.test.ts",
|
|
128
|
+
"!src/**/*.test.tsx",
|
|
129
|
+
"!src/**/*.spec.ts",
|
|
130
|
+
"!src/**/*.spec.tsx",
|
|
131
|
+
"!dist/**/__tests__/**",
|
|
132
|
+
"!dist/**/__stubs__/**",
|
|
133
|
+
"!dist/**/*.test.js",
|
|
134
|
+
"!dist/**/*.test.js.map",
|
|
135
|
+
"!dist/**/*.test.d.ts",
|
|
136
|
+
"!dist/**/*.test.d.ts.map",
|
|
137
|
+
"!dist/**/*.spec.js",
|
|
138
|
+
"!dist/**/*.spec.js.map",
|
|
139
|
+
"!dist/**/*.spec.d.ts",
|
|
140
|
+
"!dist/**/*.spec.d.ts.map"
|
|
141
|
+
],
|
|
142
|
+
"vibes": {
|
|
143
|
+
"visibility": "public-framework"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigitalOcean App Platform Deploy Client
|
|
3
|
+
*
|
|
4
|
+
* Canonical `runtime/client` (id `digitalocean-app-deploy`) for creating or
|
|
5
|
+
* updating an App Platform application from an `AppDeployment` manifest.
|
|
6
|
+
* Uses the DigitalOcean REST API v2 directly (no `doctl` dependency).
|
|
7
|
+
*
|
|
8
|
+
* Consumers resolve via `getDigitalOceanAppDeployClient()` (preferred)
|
|
9
|
+
* or `getVibesClient<DigitalOceanAppDeployClient>('digitalocean-app-deploy')`.
|
|
10
|
+
*
|
|
11
|
+
* For the initial deploy, the DO App needs a source reference. The common case
|
|
12
|
+
* is a git repo reference — the CLI writes the app spec; the operator connects
|
|
13
|
+
* the git repo in the DO dashboard once. Subsequent calls update the spec.
|
|
14
|
+
*
|
|
15
|
+
* Credentials: reads `DIGITALOCEAN_API_TOKEN` from the operator's environment.
|
|
16
|
+
* (Secrets-manager integration is tracked in `infra-deploy/SPEC.md` Migration
|
|
17
|
+
* debt — once landed, this client will resolve the token via secrets/store.)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getRuntimeEnv } from '@vibesdotdev/runtime';
|
|
21
|
+
import type { VibesRuntime } from '@vibesdotdev/runtime';
|
|
22
|
+
import {
|
|
23
|
+
BaseRuntimeClient,
|
|
24
|
+
type RuntimeClientDescriptor
|
|
25
|
+
} from '@vibesdotdev/runtime-client';
|
|
26
|
+
import type { AppDeployment } from '@vibesdotdev/infra-core/deployment';
|
|
27
|
+
|
|
28
|
+
const DO_API_BASE = 'https://api.digitalocean.com/v2';
|
|
29
|
+
|
|
30
|
+
export interface DeployAppOptions {
|
|
31
|
+
deployment: AppDeployment;
|
|
32
|
+
/** Existing DO App id (for updates). Omit to create a new App. */
|
|
33
|
+
appId?: string;
|
|
34
|
+
/** Region slug, e.g. 'nyc', 'sfo3', 'ams'. */
|
|
35
|
+
region?: string;
|
|
36
|
+
/** Git repo reference for the app source (github/gitlab). */
|
|
37
|
+
git?: {
|
|
38
|
+
repoCloneUrl?: string;
|
|
39
|
+
branch?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Environment name for DO (e.g. 'production'). */
|
|
42
|
+
environmentName?: string;
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
onLine?: (stream: 'stdout' | 'stderr', line: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DeployAppResult {
|
|
48
|
+
appId: string;
|
|
49
|
+
liveUrl?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DOEnvSpec {
|
|
53
|
+
key: string;
|
|
54
|
+
value?: string;
|
|
55
|
+
scope?: 'RUN_TIME' | 'BUILD_TIME' | 'RUN_AND_BUILD_TIME';
|
|
56
|
+
type?: 'GENERAL' | 'SECRET';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DOGitHubRef {
|
|
60
|
+
repo?: string;
|
|
61
|
+
branch?: string;
|
|
62
|
+
deploy_on_push?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface DOServiceSpec {
|
|
66
|
+
name: string;
|
|
67
|
+
instance_count?: number;
|
|
68
|
+
instance_size_slug?: string;
|
|
69
|
+
http_port?: number;
|
|
70
|
+
build_command?: string;
|
|
71
|
+
run_command?: string;
|
|
72
|
+
source_dir?: string;
|
|
73
|
+
github?: DOGitHubRef;
|
|
74
|
+
envs?: DOEnvSpec[];
|
|
75
|
+
health_check?: { http_path?: string };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface DOStaticSiteSpec {
|
|
79
|
+
name: string;
|
|
80
|
+
build_command?: string;
|
|
81
|
+
source_dir?: string;
|
|
82
|
+
output_dir?: string;
|
|
83
|
+
github?: DOGitHubRef;
|
|
84
|
+
envs?: DOEnvSpec[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface DOAppSpec {
|
|
88
|
+
name: string;
|
|
89
|
+
region?: string;
|
|
90
|
+
services?: DOServiceSpec[];
|
|
91
|
+
static_sites?: DOStaticSiteSpec[];
|
|
92
|
+
envs?: DOEnvSpec[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function authHeader(): string {
|
|
96
|
+
const token = getRuntimeEnv('DIGITALOCEAN_API_TOKEN') ?? '';
|
|
97
|
+
if (!token) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'DIGITALOCEAN_API_TOKEN is not set. Generate one at https://cloud.digitalocean.com/account/api/tokens and set it in your env.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return `Bearer ${token}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function doFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
106
|
+
const response = await fetch(`${DO_API_BASE}${path}`, {
|
|
107
|
+
...init,
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: authHeader(),
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
...(init?.headers ?? {})
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const body = await response.text();
|
|
116
|
+
throw new Error(`DO API ${response.status} ${path}: ${body}`);
|
|
117
|
+
}
|
|
118
|
+
if (response.status === 204) return undefined as T;
|
|
119
|
+
return (await response.json()) as T;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildAppSpec(deployment: AppDeployment, options: DeployAppOptions): DOAppSpec {
|
|
123
|
+
const envs: DOEnvSpec[] = deployment.env
|
|
124
|
+
.filter((requirement) => typeof requirement.value === 'string')
|
|
125
|
+
.map((requirement) => ({
|
|
126
|
+
key: requirement.name,
|
|
127
|
+
value: requirement.value as string,
|
|
128
|
+
scope: 'RUN_AND_BUILD_TIME' as const,
|
|
129
|
+
type: (requirement.secret ? 'SECRET' : 'GENERAL') as 'GENERAL' | 'SECRET'
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const github: DOGitHubRef = {
|
|
133
|
+
repo: options.git?.repoCloneUrl,
|
|
134
|
+
branch: options.git?.branch ?? 'main',
|
|
135
|
+
deploy_on_push: false
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const isStaticRuntime = deployment.runtime === 'browser' || deployment.runtime === 'edge';
|
|
139
|
+
const sourceDir = deployment.build.appDir;
|
|
140
|
+
const outputDir = deployment.build.outputDir;
|
|
141
|
+
|
|
142
|
+
if (isStaticRuntime) {
|
|
143
|
+
return {
|
|
144
|
+
name: deployment.appId,
|
|
145
|
+
region: options.region ?? 'nyc',
|
|
146
|
+
static_sites: [
|
|
147
|
+
{
|
|
148
|
+
name: deployment.appId,
|
|
149
|
+
build_command: deployment.build.buildCommand,
|
|
150
|
+
source_dir: sourceDir,
|
|
151
|
+
output_dir: outputDir,
|
|
152
|
+
github: options.git?.repoCloneUrl ? github : undefined,
|
|
153
|
+
envs
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name: deployment.appId,
|
|
161
|
+
region: options.region ?? 'nyc',
|
|
162
|
+
services: [
|
|
163
|
+
{
|
|
164
|
+
name: deployment.appId,
|
|
165
|
+
instance_count: 1,
|
|
166
|
+
instance_size_slug: 'apps-s-1vcpu-0.5gb',
|
|
167
|
+
http_port: 3000,
|
|
168
|
+
build_command: deployment.build.buildCommand,
|
|
169
|
+
run_command: 'node build',
|
|
170
|
+
source_dir: sourceDir,
|
|
171
|
+
github: options.git?.repoCloneUrl ? github : undefined,
|
|
172
|
+
envs,
|
|
173
|
+
health_check: { http_path: '/' }
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export class DigitalOceanAppDeployClient extends BaseRuntimeClient {
|
|
180
|
+
constructor(
|
|
181
|
+
descriptor: RuntimeClientDescriptor = {
|
|
182
|
+
id: 'digitalocean-app-deploy',
|
|
183
|
+
kind: 'runtime/client'
|
|
184
|
+
},
|
|
185
|
+
context?: unknown,
|
|
186
|
+
runtime?: VibesRuntime
|
|
187
|
+
) {
|
|
188
|
+
super(descriptor, context, runtime);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async deployApp(options: DeployAppOptions): Promise<DeployAppResult> {
|
|
192
|
+
const { deployment, onLine } = options;
|
|
193
|
+
const spec = buildAppSpec(deployment, options);
|
|
194
|
+
|
|
195
|
+
if (options.dryRun) {
|
|
196
|
+
onLine?.('stdout', `[dry-run] DO App spec for '${deployment.appId}':`);
|
|
197
|
+
onLine?.('stdout', JSON.stringify(spec, null, 2));
|
|
198
|
+
return { appId: options.appId ?? '(new)' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options.appId) {
|
|
202
|
+
onLine?.('stdout', `Updating DO App ${options.appId}...`);
|
|
203
|
+
const updated = await doFetch<{ app: { id: string; live_url?: string; spec?: DOAppSpec } }>(
|
|
204
|
+
`/apps/${options.appId}`,
|
|
205
|
+
{
|
|
206
|
+
method: 'PUT',
|
|
207
|
+
body: JSON.stringify({ spec })
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
return {
|
|
211
|
+
appId: updated.app.id,
|
|
212
|
+
liveUrl: updated.app.live_url
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
onLine?.('stdout', `Creating DO App '${deployment.appId}'...`);
|
|
217
|
+
const created = await doFetch<{ app: { id: string; live_url?: string } }>(`/apps`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
body: JSON.stringify({ spec })
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
appId: created.app.id,
|
|
223
|
+
liveUrl: created.app.live_url
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface for the infra-doks runtime clients.
|
|
3
|
+
*
|
|
4
|
+
* Consumers resolve the App Platform deploy client via
|
|
5
|
+
* `getDigitalOceanAppDeployClient()` (preferred) or, for type-pinned generic
|
|
6
|
+
* resolution, `getVibesClient<DigitalOceanAppDeployClient>('digitalocean-app-deploy')`.
|
|
7
|
+
*
|
|
8
|
+
* Requires `doksPlugin` to be registered with the runtime so the
|
|
9
|
+
* `runtime/client` descriptor + loader for id `digitalocean-app-deploy` are
|
|
10
|
+
* resolvable.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getVibesClient } from '@vibesdotdev/runtime-client';
|
|
14
|
+
import { DigitalOceanAppDeployClient } from './digitalocean-app-deploy.client';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
DigitalOceanAppDeployClient,
|
|
18
|
+
type DeployAppOptions,
|
|
19
|
+
type DeployAppResult
|
|
20
|
+
} from './digitalocean-app-deploy.client';
|
|
21
|
+
|
|
22
|
+
export async function getDigitalOceanAppDeployClient(): Promise<DigitalOceanAppDeployClient> {
|
|
23
|
+
return getVibesClient<DigitalOceanAppDeployClient>('digitalocean-app-deploy');
|
|
24
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Cloud Provider Implementation
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class that provides common functionality for all cloud providers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
CloudProvider,
|
|
9
|
+
CloudInstance,
|
|
10
|
+
InstanceConfig,
|
|
11
|
+
InstanceFilters,
|
|
12
|
+
CloudVolume,
|
|
13
|
+
VolumeConfig,
|
|
14
|
+
VolumeFilters,
|
|
15
|
+
SSHKey
|
|
16
|
+
} from '../types.ts';
|
|
17
|
+
import { CloudProviderError } from '../types.ts';
|
|
18
|
+
|
|
19
|
+
export abstract class BaseCloudProvider implements CloudProvider {
|
|
20
|
+
abstract name: string;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
protected credentials: string,
|
|
24
|
+
protected defaultRegion?: string
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
// Abstract methods that must be implemented by each provider
|
|
28
|
+
abstract createInstance(config: InstanceConfig): Promise<CloudInstance>;
|
|
29
|
+
abstract deleteInstance(instanceId: string): Promise<void>;
|
|
30
|
+
abstract getInstance(instanceId: string): Promise<CloudInstance | null>;
|
|
31
|
+
abstract listInstances(filters?: InstanceFilters): Promise<CloudInstance[]>;
|
|
32
|
+
|
|
33
|
+
abstract createVolume(config: VolumeConfig): Promise<CloudVolume>;
|
|
34
|
+
abstract attachVolume(volumeId: string, instanceId: string): Promise<void>;
|
|
35
|
+
abstract detachVolume(volumeId: string): Promise<void>;
|
|
36
|
+
abstract deleteVolume(volumeId: string): Promise<void>;
|
|
37
|
+
abstract listVolumes(filters?: VolumeFilters): Promise<CloudVolume[]>;
|
|
38
|
+
|
|
39
|
+
abstract getOrCreateVPC(name: string): Promise<string>;
|
|
40
|
+
abstract addSSHKey(name: string, publicKey: string): Promise<string>;
|
|
41
|
+
abstract listSSHKeys(): Promise<SSHKey[]>;
|
|
42
|
+
|
|
43
|
+
// Common helper methods
|
|
44
|
+
protected generateInstanceName(prefix: string = 'gpu-instance'): string {
|
|
45
|
+
const timestamp = Date.now();
|
|
46
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
47
|
+
return `${prefix}-${timestamp}-${random}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected generateVolumeName(prefix: string = 'gpu-volume'): string {
|
|
51
|
+
const timestamp = Date.now();
|
|
52
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
53
|
+
return `${prefix}-${timestamp}-${random}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected async waitForInstanceStatus(
|
|
57
|
+
instanceId: string,
|
|
58
|
+
targetStatus: string,
|
|
59
|
+
timeoutMs: number = 300000,
|
|
60
|
+
pollIntervalMs: number = 5000
|
|
61
|
+
): Promise<CloudInstance> {
|
|
62
|
+
const startTime = Date.now();
|
|
63
|
+
|
|
64
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
65
|
+
const instance = await this.getInstance(instanceId);
|
|
66
|
+
|
|
67
|
+
if (!instance) {
|
|
68
|
+
throw this.createError('Instance not found', 'INSTANCE_NOT_FOUND', { instanceId });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (instance.status === targetStatus) {
|
|
72
|
+
return instance;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (instance.status === 'error' || instance.status === 'terminated') {
|
|
76
|
+
throw this.createError(
|
|
77
|
+
`Instance entered error state: ${instance.status}`,
|
|
78
|
+
'INSTANCE_ERROR',
|
|
79
|
+
{ instanceId, status: instance.status }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await this.sleep(pollIntervalMs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw this.createError(`Timeout waiting for instance status: ${targetStatus}`, 'TIMEOUT', {
|
|
87
|
+
instanceId,
|
|
88
|
+
targetStatus,
|
|
89
|
+
timeoutMs
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected async waitForVolumeStatus(
|
|
94
|
+
volumeId: string,
|
|
95
|
+
targetStatus: string,
|
|
96
|
+
timeoutMs: number = 120000,
|
|
97
|
+
pollIntervalMs: number = 3000
|
|
98
|
+
): Promise<CloudVolume> {
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
|
|
101
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
102
|
+
const volumes = await this.listVolumes();
|
|
103
|
+
const volume = volumes.find((v) => v.id === volumeId);
|
|
104
|
+
|
|
105
|
+
if (!volume) {
|
|
106
|
+
throw this.createError('Volume not found', 'VOLUME_NOT_FOUND', { volumeId });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (volume.status === targetStatus) {
|
|
110
|
+
return volume;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (volume.status === 'error' || volume.status === 'deleted') {
|
|
114
|
+
throw this.createError(`Volume entered error state: ${volume.status}`, 'VOLUME_ERROR', {
|
|
115
|
+
volumeId,
|
|
116
|
+
status: volume.status
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await this.sleep(pollIntervalMs);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw this.createError(`Timeout waiting for volume status: ${targetStatus}`, 'TIMEOUT', {
|
|
124
|
+
volumeId,
|
|
125
|
+
targetStatus,
|
|
126
|
+
timeoutMs
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected createError(message: string, code: string, details?: unknown): CloudProviderError {
|
|
131
|
+
return new CloudProviderError(message, code, this.name, details);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected sleep(ms: number): Promise<void> {
|
|
135
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected buildTags(
|
|
139
|
+
tags: Record<string, string>,
|
|
140
|
+
additionalTags?: Record<string, string>
|
|
141
|
+
): Record<string, string> {
|
|
142
|
+
return {
|
|
143
|
+
'managed-by': 'vibes',
|
|
144
|
+
provider: this.name,
|
|
145
|
+
...tags,
|
|
146
|
+
...additionalTags
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|