@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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigitalOcean Cloud Provider Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements the CloudProvider interface for DigitalOcean.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseCloudProvider } from './base.ts';
|
|
8
|
+
import type {
|
|
9
|
+
CloudInstance,
|
|
10
|
+
InstanceConfig,
|
|
11
|
+
InstanceFilters,
|
|
12
|
+
CloudVolume,
|
|
13
|
+
VolumeConfig,
|
|
14
|
+
VolumeFilters,
|
|
15
|
+
SSHKey,
|
|
16
|
+
InstanceStatus
|
|
17
|
+
} from '../types.ts';
|
|
18
|
+
|
|
19
|
+
interface DODroplet {
|
|
20
|
+
id: number;
|
|
21
|
+
name: string;
|
|
22
|
+
status: string;
|
|
23
|
+
size: { slug: string };
|
|
24
|
+
region: { slug: string };
|
|
25
|
+
image: { slug: string };
|
|
26
|
+
networks: {
|
|
27
|
+
v4: Array<{ ip_address: string; type: string }>;
|
|
28
|
+
};
|
|
29
|
+
created_at: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface DOVolume {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
size_gigabytes: number;
|
|
37
|
+
region: { slug: string };
|
|
38
|
+
droplet_ids: number[];
|
|
39
|
+
created_at: string;
|
|
40
|
+
tags: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DOSSHKey {
|
|
44
|
+
id: number;
|
|
45
|
+
fingerprint: string;
|
|
46
|
+
public_key: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface DOResponseMap {
|
|
51
|
+
'/droplets': { droplet: DODroplet } | { droplets: DODroplet[] };
|
|
52
|
+
'/volumes': { volume: DOVolume } | { volumes: DOVolume[] };
|
|
53
|
+
'/vpcs': { vpc: { id: string }; vpcs: Array<{ id: string; name: string }> } | { vpc: { id: string } };
|
|
54
|
+
'/account/keys': { ssh_key: DOSSHKey } | { ssh_keys: DOSSHKey[] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type DOResponse<T> = T extends keyof DOResponseMap ? DOResponseMap[T] : Record<string, unknown>;
|
|
58
|
+
|
|
59
|
+
export class DigitalOceanProvider extends BaseCloudProvider {
|
|
60
|
+
name = 'digitalocean';
|
|
61
|
+
private readonly apiBase = 'https://api.digitalocean.com/v2';
|
|
62
|
+
|
|
63
|
+
constructor(apiToken: string, defaultRegion: string = 'nyc3') {
|
|
64
|
+
super(apiToken, defaultRegion);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async doRequest<T = Record<string, unknown>>(path: string, options: RequestInit = {}): Promise<T> {
|
|
68
|
+
const response = await fetch(`${this.apiBase}${path}`, {
|
|
69
|
+
...options,
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${this.credentials}`,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
...options.headers
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const error = await response.text();
|
|
79
|
+
throw this.createError(`DigitalOcean API error: ${response.status}`, 'API_ERROR', {
|
|
80
|
+
status: response.status,
|
|
81
|
+
error
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return response.json() as Promise<T>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async createInstance(config: InstanceConfig): Promise<CloudInstance> {
|
|
89
|
+
const dropletData = {
|
|
90
|
+
name: config.name,
|
|
91
|
+
region: config.region || this.defaultRegion,
|
|
92
|
+
size: config.type,
|
|
93
|
+
image: config.image,
|
|
94
|
+
ssh_keys: config.sshKeys || [],
|
|
95
|
+
vpc_uuid: config.vpcId,
|
|
96
|
+
tags: this.formatTags(this.buildTags(config.tags)),
|
|
97
|
+
user_data: config.userData
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const response = await this.doRequest<DOResponse<'/droplets'>>('/droplets', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: JSON.stringify(dropletData)
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const droplet = (response as { droplet: DODroplet }).droplet;
|
|
106
|
+
|
|
107
|
+
// Wait for droplet to be ready
|
|
108
|
+
const readyDroplet = await this.waitForDropletReady(droplet.id);
|
|
109
|
+
|
|
110
|
+
return this.mapDropletToInstance(readyDroplet);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async deleteInstance(instanceId: string): Promise<void> {
|
|
114
|
+
const numericId = this.extractDropletId(instanceId);
|
|
115
|
+
|
|
116
|
+
await this.doRequest(`/droplets/${numericId}`, {
|
|
117
|
+
method: 'DELETE'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getInstance(instanceId: string): Promise<CloudInstance | null> {
|
|
122
|
+
try {
|
|
123
|
+
const numericId = this.extractDropletId(instanceId);
|
|
124
|
+
const response = await this.doRequest<{ droplet: DODroplet }>(`/droplets/${numericId}`);
|
|
125
|
+
return this.mapDropletToInstance(response.droplet);
|
|
126
|
+
} catch (error: unknown) {
|
|
127
|
+
if ((error as { details?: { status: number } }).details?.status === 404) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async listInstances(filters?: InstanceFilters): Promise<CloudInstance[]> {
|
|
135
|
+
const params = new URLSearchParams();
|
|
136
|
+
if (filters?.tags) {
|
|
137
|
+
params.append('tag_name', this.formatTags(filters.tags).join(','));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const response = await this.doRequest<{ droplets: DODroplet[] }>(`/droplets?${params.toString()}`);
|
|
141
|
+
const droplets = response.droplets;
|
|
142
|
+
|
|
143
|
+
let instances = droplets.map((d) => this.mapDropletToInstance(d));
|
|
144
|
+
|
|
145
|
+
// Apply additional filters
|
|
146
|
+
if (filters?.status) {
|
|
147
|
+
instances = instances.filter((i) => filters.status!.includes(i.status));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (filters?.region) {
|
|
151
|
+
instances = instances.filter((i) => i.region === filters.region);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return instances;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async createVolume(config: VolumeConfig): Promise<CloudVolume> {
|
|
158
|
+
const volumeData = {
|
|
159
|
+
name: config.name,
|
|
160
|
+
size_gigabytes: config.size,
|
|
161
|
+
region: config.region || this.defaultRegion,
|
|
162
|
+
tags: config.tags ? this.formatTags(config.tags) : []
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const response = await this.doRequest<{ volume: DOVolume }>('/volumes', {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
body: JSON.stringify(volumeData)
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return this.mapVolumeToCloudVolume(response.volume);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async attachVolume(volumeId: string, instanceId: string): Promise<void> {
|
|
174
|
+
const numericId = this.extractDropletId(instanceId);
|
|
175
|
+
|
|
176
|
+
await this.doRequest(`/volumes/${volumeId}/actions`, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
type: 'attach',
|
|
180
|
+
droplet_id: numericId
|
|
181
|
+
})
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Wait for volume to be attached
|
|
185
|
+
await this.waitForVolumeStatus(volumeId, 'in-use');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async detachVolume(volumeId: string): Promise<void> {
|
|
189
|
+
await this.doRequest(`/volumes/${volumeId}/actions`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
type: 'detach'
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Wait for volume to be available
|
|
197
|
+
await this.waitForVolumeStatus(volumeId, 'available');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async deleteVolume(volumeId: string): Promise<void> {
|
|
201
|
+
await this.doRequest(`/volumes/${volumeId}`, {
|
|
202
|
+
method: 'DELETE'
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async listVolumes(filters?: VolumeFilters): Promise<CloudVolume[]> {
|
|
207
|
+
const params = new URLSearchParams();
|
|
208
|
+
if (filters?.region) {
|
|
209
|
+
params.append('region', filters.region);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const response = await this.doRequest<{ volumes: DOVolume[] }>(`/volumes?${params.toString()}`);
|
|
213
|
+
const volumes = response.volumes;
|
|
214
|
+
|
|
215
|
+
return volumes.map((v) => this.mapVolumeToCloudVolume(v));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getOrCreateVPC(name: string): Promise<string> {
|
|
219
|
+
// List existing VPCs
|
|
220
|
+
const response = await this.doRequest<{ vpcs?: Array<{ id: string; name: string }> }>('/vpcs');
|
|
221
|
+
const vpcs = response.vpcs || [];
|
|
222
|
+
|
|
223
|
+
// Check if VPC with this name exists
|
|
224
|
+
const existingVpc = vpcs.find((vpc: Record<string, unknown>) => vpc.name === name);
|
|
225
|
+
if (existingVpc) {
|
|
226
|
+
return existingVpc.id;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Create new VPC
|
|
230
|
+
const vpcData = {
|
|
231
|
+
name,
|
|
232
|
+
region: this.defaultRegion,
|
|
233
|
+
ip_range: '10.0.0.0/16'
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const createResponse = await this.doRequest<{ vpc: { id: string } }>('/vpcs', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
body: JSON.stringify(vpcData)
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return createResponse.vpc.id;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async addSSHKey(name: string, publicKey: string): Promise<string> {
|
|
245
|
+
const response = await this.doRequest<{ ssh_key: DOSSHKey }>('/account/keys', {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
name,
|
|
249
|
+
public_key: publicKey
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return response.ssh_key.fingerprint;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async listSSHKeys(): Promise<SSHKey[]> {
|
|
257
|
+
const response = await this.doRequest<{ ssh_keys: DOSSHKey[] }>('/account/keys');
|
|
258
|
+
const keys = response.ssh_keys;
|
|
259
|
+
|
|
260
|
+
return keys.map((key) => ({
|
|
261
|
+
id: key.fingerprint,
|
|
262
|
+
name: key.name,
|
|
263
|
+
fingerprint: key.fingerprint,
|
|
264
|
+
publicKey: key.public_key
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Helper methods
|
|
269
|
+
private async waitForDropletReady(dropletId: number): Promise<DODroplet> {
|
|
270
|
+
const startTime = Date.now();
|
|
271
|
+
const timeout = 300000;
|
|
272
|
+
const pollInterval = 5000;
|
|
273
|
+
|
|
274
|
+
while (Date.now() - startTime < timeout) {
|
|
275
|
+
const response = await this.doRequest<{ droplet: DODroplet }>(`/droplets/${dropletId}`);
|
|
276
|
+
const droplet = response.droplet;
|
|
277
|
+
|
|
278
|
+
if (droplet.status === 'active') {
|
|
279
|
+
return droplet;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (droplet.status === 'error' || droplet.status === 'archive') {
|
|
283
|
+
throw this.createError(`Droplet entered error state: ${droplet.status}`, 'DROPLET_ERROR', {
|
|
284
|
+
dropletId,
|
|
285
|
+
status: droplet.status
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await this.sleep(pollInterval);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw this.createError('Timeout waiting for droplet to be ready', 'TIMEOUT', {
|
|
293
|
+
dropletId,
|
|
294
|
+
timeout
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private mapDropletToInstance(droplet: DODroplet): CloudInstance {
|
|
299
|
+
const publicIp = droplet.networks.v4.find((n) => n.type === 'public')?.ip_address;
|
|
300
|
+
const privateIp = droplet.networks.v4.find((n) => n.type === 'private')?.ip_address;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
id: `do-${droplet.id}`,
|
|
304
|
+
name: droplet.name,
|
|
305
|
+
type: droplet.size.slug,
|
|
306
|
+
region: droplet.region.slug,
|
|
307
|
+
status: this.mapDropletStatus(droplet.status),
|
|
308
|
+
publicIp,
|
|
309
|
+
privateIp,
|
|
310
|
+
createdAt: new Date(droplet.created_at),
|
|
311
|
+
tags: this.parseTags(droplet.tags)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private mapDropletStatus(doStatus: string): InstanceStatus {
|
|
316
|
+
const statusMap: Record<string, InstanceStatus> = {
|
|
317
|
+
new: 'pending',
|
|
318
|
+
active: 'running',
|
|
319
|
+
off: 'stopped',
|
|
320
|
+
archive: 'terminated',
|
|
321
|
+
error: 'error'
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return statusMap[doStatus] || 'pending';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private mapVolumeToCloudVolume(volume: DOVolume): CloudVolume {
|
|
328
|
+
return {
|
|
329
|
+
id: volume.id,
|
|
330
|
+
name: volume.name,
|
|
331
|
+
size: volume.size_gigabytes,
|
|
332
|
+
region: volume.region.slug,
|
|
333
|
+
status: volume.droplet_ids.length > 0 ? 'in-use' : 'available',
|
|
334
|
+
attachedTo: volume.droplet_ids.length > 0 ? `do-${volume.droplet_ids[0]}` : undefined,
|
|
335
|
+
createdAt: new Date(volume.created_at),
|
|
336
|
+
tags: this.parseTags(volume.tags)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private extractDropletId(instanceId: string): number {
|
|
341
|
+
if (instanceId.startsWith('do-')) {
|
|
342
|
+
return parseInt(instanceId.substring(3), 10);
|
|
343
|
+
}
|
|
344
|
+
return parseInt(instanceId, 10);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private formatTags(tags: Record<string, string>): string[] {
|
|
348
|
+
return Object.entries(tags).map(([key, value]) => `${key}:${value}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private parseTags(tags: string[]): Record<string, string> {
|
|
352
|
+
const result: Record<string, string> = {};
|
|
353
|
+
|
|
354
|
+
for (const tag of tags) {
|
|
355
|
+
const [key, value] = tag.split(':');
|
|
356
|
+
if (key && value) {
|
|
357
|
+
result[key] = value;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates cloud provider instances based on configuration.
|
|
5
|
+
* Unsupported providers return a stub that throws {@link CloudProviderError}
|
|
6
|
+
* on every operation instead of crashing at construction time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getRuntimeEnv } from '@vibesdotdev/runtime';
|
|
10
|
+
import { getLogger } from '@vibesdotdev/logging';
|
|
11
|
+
import type {
|
|
12
|
+
CloudProvider,
|
|
13
|
+
CloudProviderConfig,
|
|
14
|
+
CloudInstance,
|
|
15
|
+
InstanceConfig,
|
|
16
|
+
InstanceFilters,
|
|
17
|
+
CloudVolume,
|
|
18
|
+
VolumeConfig,
|
|
19
|
+
VolumeFilters,
|
|
20
|
+
SSHKey
|
|
21
|
+
} from '../types.ts';
|
|
22
|
+
import { CloudProviderError } from '../types.ts';
|
|
23
|
+
import { DigitalOceanProvider } from './digitalocean.ts';
|
|
24
|
+
|
|
25
|
+
const logger = getLogger('infra-doks:cloud');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stub returned for providers that are recognised but not yet implemented.
|
|
29
|
+
*
|
|
30
|
+
* Logs a warning once at construction and throws a structured
|
|
31
|
+
* {@link CloudProviderError} on every method call so callers can distinguish
|
|
32
|
+
* "provider not available" from "unknown provider" without catching raw Errors.
|
|
33
|
+
*/
|
|
34
|
+
class UnsupportedCloudProvider implements CloudProvider {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
|
|
37
|
+
constructor(providerType: string) {
|
|
38
|
+
this.name = providerType;
|
|
39
|
+
logger.warn(`${providerType} cloud provider requested but is not yet implemented`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private unsupported(): never {
|
|
43
|
+
throw new CloudProviderError(
|
|
44
|
+
`${this.name} provider is not yet implemented`,
|
|
45
|
+
'PROVIDER_NOT_IMPLEMENTED',
|
|
46
|
+
this.name
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
createInstance(_config: InstanceConfig): Promise<CloudInstance> {
|
|
51
|
+
return this.unsupported();
|
|
52
|
+
}
|
|
53
|
+
deleteInstance(_instanceId: string): Promise<void> {
|
|
54
|
+
return this.unsupported();
|
|
55
|
+
}
|
|
56
|
+
getInstance(_instanceId: string): Promise<CloudInstance | null> {
|
|
57
|
+
return this.unsupported();
|
|
58
|
+
}
|
|
59
|
+
listInstances(_filters?: InstanceFilters): Promise<CloudInstance[]> {
|
|
60
|
+
return this.unsupported();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
createVolume(_config: VolumeConfig): Promise<CloudVolume> {
|
|
64
|
+
return this.unsupported();
|
|
65
|
+
}
|
|
66
|
+
attachVolume(_volumeId: string, _instanceId: string): Promise<void> {
|
|
67
|
+
return this.unsupported();
|
|
68
|
+
}
|
|
69
|
+
detachVolume(_volumeId: string): Promise<void> {
|
|
70
|
+
return this.unsupported();
|
|
71
|
+
}
|
|
72
|
+
deleteVolume(_volumeId: string): Promise<void> {
|
|
73
|
+
return this.unsupported();
|
|
74
|
+
}
|
|
75
|
+
listVolumes(_filters?: VolumeFilters): Promise<CloudVolume[]> {
|
|
76
|
+
return this.unsupported();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getOrCreateVPC(_name: string): Promise<string> {
|
|
80
|
+
return this.unsupported();
|
|
81
|
+
}
|
|
82
|
+
addSSHKey(_name: string, _publicKey: string): Promise<string> {
|
|
83
|
+
return this.unsupported();
|
|
84
|
+
}
|
|
85
|
+
listSSHKeys(): Promise<SSHKey[]> {
|
|
86
|
+
return this.unsupported();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a cloud provider instance.
|
|
92
|
+
*
|
|
93
|
+
* For known-but-unimplemented providers (aws, gcp, azure) the returned
|
|
94
|
+
* instance is valid but every method call throws a structured
|
|
95
|
+
* {@link CloudProviderError} with code `PROVIDER_NOT_IMPLEMENTED`.
|
|
96
|
+
*
|
|
97
|
+
* For completely unknown provider types a raw {@link Error} is still thrown
|
|
98
|
+
* at construction time because the caller has passed an invalid configuration.
|
|
99
|
+
*/
|
|
100
|
+
export function createCloudProvider(config: CloudProviderConfig): CloudProvider {
|
|
101
|
+
switch (config.type) {
|
|
102
|
+
case 'digitalocean':
|
|
103
|
+
return new DigitalOceanProvider(config.credentials, config.region);
|
|
104
|
+
|
|
105
|
+
case 'aws':
|
|
106
|
+
return new UnsupportedCloudProvider(config.type);
|
|
107
|
+
|
|
108
|
+
case 'gcp':
|
|
109
|
+
return new UnsupportedCloudProvider(config.type);
|
|
110
|
+
|
|
111
|
+
case 'azure':
|
|
112
|
+
return new UnsupportedCloudProvider(config.type);
|
|
113
|
+
|
|
114
|
+
default: {
|
|
115
|
+
// Exhaustiveness check: this should never be reached if config.type
|
|
116
|
+
// is one of the known providers. TypeScript should narrow config to
|
|
117
|
+
// never here, but we use a type assertion to handle edge cases.
|
|
118
|
+
throw new Error(`Unknown cloud provider type: ${(config as CloudProviderConfig).type}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get cloud provider from environment configuration
|
|
125
|
+
*/
|
|
126
|
+
export function getCloudProviderFromEnv(): CloudProvider | null {
|
|
127
|
+
// Check for DigitalOcean
|
|
128
|
+
const doToken = getRuntimeEnv('DIGITALOCEAN_TOKEN');
|
|
129
|
+
if (doToken) {
|
|
130
|
+
return createCloudProvider({
|
|
131
|
+
type: 'digitalocean',
|
|
132
|
+
credentials: doToken,
|
|
133
|
+
region: getRuntimeEnv('GPU_REGION') || 'nyc3'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for AWS
|
|
138
|
+
const awsAccessKey = getRuntimeEnv('AWS_ACCESS_KEY_ID');
|
|
139
|
+
const awsSecretKey = getRuntimeEnv('AWS_SECRET_ACCESS_KEY');
|
|
140
|
+
if (awsAccessKey && awsSecretKey) {
|
|
141
|
+
logger.warn('AWS credentials detected but AWS provider is not yet implemented');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const gcpCredentials = getRuntimeEnv('GOOGLE_APPLICATION_CREDENTIALS');
|
|
145
|
+
if (gcpCredentials) {
|
|
146
|
+
logger.warn('GCP provider detected but not yet implemented');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const azureClientId = getRuntimeEnv('AZURE_CLIENT_ID');
|
|
150
|
+
if (azureClientId) {
|
|
151
|
+
logger.warn('Azure provider detected but not yet implemented');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validate cloud provider configuration
|
|
159
|
+
*/
|
|
160
|
+
export function validateCloudProviderConfig(config: CloudProviderConfig): string[] {
|
|
161
|
+
const errors: string[] = [];
|
|
162
|
+
|
|
163
|
+
if (!config.type) {
|
|
164
|
+
errors.push('Cloud provider type is required');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!config.credentials) {
|
|
168
|
+
errors.push('Cloud provider credentials are required');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Provider-specific validation
|
|
172
|
+
switch (config.type) {
|
|
173
|
+
case 'digitalocean':
|
|
174
|
+
if (!config.credentials || config.credentials.length < 20) {
|
|
175
|
+
errors.push('Invalid DigitalOcean API token');
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case 'aws':
|
|
180
|
+
case 'gcp':
|
|
181
|
+
case 'azure':
|
|
182
|
+
errors.push(`${config.type} provider not yet implemented`);
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
errors.push(`Unknown cloud provider type: ${config.type}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return errors;
|
|
190
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Provider Abstraction Layer
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified interface for managing cloud resources across
|
|
5
|
+
* different providers (DigitalOcean, AWS, GCP, Azure).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
K8sMetadata,
|
|
10
|
+
K8sLabelSelector,
|
|
11
|
+
K8sResourceQuantity,
|
|
12
|
+
K8sResourceRequirements,
|
|
13
|
+
K8sEnvFromSource,
|
|
14
|
+
K8sEnvVar,
|
|
15
|
+
K8sProbe,
|
|
16
|
+
K8sContainerPort,
|
|
17
|
+
K8sVolumeMount,
|
|
18
|
+
K8sContainer,
|
|
19
|
+
K8sConfigMapKeyToPath,
|
|
20
|
+
K8sVolume,
|
|
21
|
+
K8sPodSpec,
|
|
22
|
+
K8sPodTemplateSpec,
|
|
23
|
+
K8sDeployment,
|
|
24
|
+
K8sPersistentVolumeClaimSpec,
|
|
25
|
+
K8sVolumeClaimTemplate,
|
|
26
|
+
K8sStatefulSet,
|
|
27
|
+
K8sServicePort,
|
|
28
|
+
K8sService,
|
|
29
|
+
K8sConfigMap,
|
|
30
|
+
K8sSecret,
|
|
31
|
+
DOManagedDBSpec,
|
|
32
|
+
DOManagedRedisSpec,
|
|
33
|
+
DOSpacesSpec,
|
|
34
|
+
CloudProvider,
|
|
35
|
+
InstanceConfig,
|
|
36
|
+
CloudInstance,
|
|
37
|
+
InstanceFilters,
|
|
38
|
+
VolumeConfig,
|
|
39
|
+
CloudVolume,
|
|
40
|
+
VolumeFilters,
|
|
41
|
+
SSHKey,
|
|
42
|
+
CloudProviderConfig,
|
|
43
|
+
InstanceStatus,
|
|
44
|
+
VolumeStatus,
|
|
45
|
+
K8sConfig,
|
|
46
|
+
K8sEnvironment,
|
|
47
|
+
PodSelector,
|
|
48
|
+
PodStatus,
|
|
49
|
+
LogOptions,
|
|
50
|
+
LogEntry,
|
|
51
|
+
LogStream,
|
|
52
|
+
DeploymentStatus,
|
|
53
|
+
ServiceInfo,
|
|
54
|
+
PodMetrics,
|
|
55
|
+
ResourceMetrics,
|
|
56
|
+
K8sEvent,
|
|
57
|
+
ExecOptions,
|
|
58
|
+
ExecResult,
|
|
59
|
+
PortForwardOptions,
|
|
60
|
+
PortForward,
|
|
61
|
+
PodPhase,
|
|
62
|
+
ConditionStatus,
|
|
63
|
+
DeploymentConditionType,
|
|
64
|
+
ServiceType,
|
|
65
|
+
ServiceProtocol,
|
|
66
|
+
EventType,
|
|
67
|
+
ContainerState,
|
|
68
|
+
} from '../types.ts';
|
|
69
|
+
export {
|
|
70
|
+
CloudProviderError,
|
|
71
|
+
K8sError,
|
|
72
|
+
K8sAuthError,
|
|
73
|
+
K8sNotFoundError,
|
|
74
|
+
} from '../types.ts';
|
|
75
|
+
export { BaseCloudProvider } from './base.ts';
|
|
76
|
+
export { DigitalOceanProvider } from './digitalocean.ts';
|
|
77
|
+
export {
|
|
78
|
+
createCloudProvider,
|
|
79
|
+
getCloudProviderFromEnv,
|
|
80
|
+
validateCloudProviderConfig
|
|
81
|
+
} from './factory.ts';
|