@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.
Files changed (113) hide show
  1. package/README.md +107 -0
  2. package/SPEC.md +285 -0
  3. package/dist/client/digitalocean-app-deploy.client.d.ts +46 -0
  4. package/dist/client/digitalocean-app-deploy.client.d.ts.map +1 -0
  5. package/dist/client/digitalocean-app-deploy.client.js +135 -0
  6. package/dist/client/digitalocean-app-deploy.client.js.map +1 -0
  7. package/dist/client/index.d.ts +15 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +18 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/cloud/base.d.ts +33 -0
  12. package/dist/cloud/base.d.ts.map +1 -0
  13. package/dist/cloud/base.js +86 -0
  14. package/dist/cloud/base.js.map +1 -0
  15. package/dist/cloud/digitalocean.d.ts +33 -0
  16. package/dist/cloud/digitalocean.d.ts.map +1 -0
  17. package/dist/cloud/digitalocean.js +258 -0
  18. package/dist/cloud/digitalocean.js.map +1 -0
  19. package/dist/cloud/factory.d.ts +28 -0
  20. package/dist/cloud/factory.d.ts.map +1 -0
  21. package/dist/cloud/factory.js +151 -0
  22. package/dist/cloud/factory.js.map +1 -0
  23. package/dist/cloud/index.d.ts +12 -0
  24. package/dist/cloud/index.d.ts.map +1 -0
  25. package/dist/cloud/index.js +11 -0
  26. package/dist/cloud/index.js.map +1 -0
  27. package/dist/doks.plugin.d.ts +41 -0
  28. package/dist/doks.plugin.d.ts.map +1 -0
  29. package/dist/doks.plugin.js +287 -0
  30. package/dist/doks.plugin.js.map +1 -0
  31. package/dist/implementations/deployment.impl.d.ts +34 -0
  32. package/dist/implementations/deployment.impl.d.ts.map +1 -0
  33. package/dist/implementations/deployment.impl.js +86 -0
  34. package/dist/implementations/deployment.impl.js.map +1 -0
  35. package/dist/implementations/droplet.impl.d.ts +85 -0
  36. package/dist/implementations/droplet.impl.d.ts.map +1 -0
  37. package/dist/implementations/droplet.impl.js +113 -0
  38. package/dist/implementations/droplet.impl.js.map +1 -0
  39. package/dist/implementations/gitea.impl.d.ts +68 -0
  40. package/dist/implementations/gitea.impl.d.ts.map +1 -0
  41. package/dist/implementations/gitea.impl.js +295 -0
  42. package/dist/implementations/gitea.impl.js.map +1 -0
  43. package/dist/implementations/managed-db.impl.d.ts +25 -0
  44. package/dist/implementations/managed-db.impl.d.ts.map +1 -0
  45. package/dist/implementations/managed-db.impl.js +31 -0
  46. package/dist/implementations/managed-db.impl.js.map +1 -0
  47. package/dist/implementations/managed-redis.impl.d.ts +37 -0
  48. package/dist/implementations/managed-redis.impl.d.ts.map +1 -0
  49. package/dist/implementations/managed-redis.impl.js +40 -0
  50. package/dist/implementations/managed-redis.impl.js.map +1 -0
  51. package/dist/implementations/spaces.impl.d.ts +36 -0
  52. package/dist/implementations/spaces.impl.d.ts.map +1 -0
  53. package/dist/implementations/spaces.impl.js +40 -0
  54. package/dist/implementations/spaces.impl.js.map +1 -0
  55. package/dist/implementations/statefulset.impl.d.ts +65 -0
  56. package/dist/implementations/statefulset.impl.d.ts.map +1 -0
  57. package/dist/implementations/statefulset.impl.js +165 -0
  58. package/dist/implementations/statefulset.impl.js.map +1 -0
  59. package/dist/implementations/verdaccio.impl.d.ts +65 -0
  60. package/dist/implementations/verdaccio.impl.d.ts.map +1 -0
  61. package/dist/implementations/verdaccio.impl.js +259 -0
  62. package/dist/implementations/verdaccio.impl.js.map +1 -0
  63. package/dist/index.d.ts +15 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +19 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/kubernetes/index.d.ts +95 -0
  68. package/dist/kubernetes/index.d.ts.map +1 -0
  69. package/dist/kubernetes/index.js +625 -0
  70. package/dist/kubernetes/index.js.map +1 -0
  71. package/dist/secrets/index.d.ts +4 -0
  72. package/dist/secrets/index.d.ts.map +1 -0
  73. package/dist/secrets/index.js +4 -0
  74. package/dist/secrets/index.js.map +1 -0
  75. package/dist/secrets/vault.descriptor.d.ts +10 -0
  76. package/dist/secrets/vault.descriptor.d.ts.map +1 -0
  77. package/dist/secrets/vault.descriptor.js +25 -0
  78. package/dist/secrets/vault.descriptor.js.map +1 -0
  79. package/dist/secrets/vault.impl.cloud.d.ts +40 -0
  80. package/dist/secrets/vault.impl.cloud.d.ts.map +1 -0
  81. package/dist/secrets/vault.impl.cloud.js +178 -0
  82. package/dist/secrets/vault.impl.cloud.js.map +1 -0
  83. package/dist/secrets/vault.impl.d.ts +29 -0
  84. package/dist/secrets/vault.impl.d.ts.map +1 -0
  85. package/dist/secrets/vault.impl.js +137 -0
  86. package/dist/secrets/vault.impl.js.map +1 -0
  87. package/dist/types.d.ts +509 -0
  88. package/dist/types.d.ts.map +1 -0
  89. package/dist/types.js +47 -0
  90. package/dist/types.js.map +1 -0
  91. package/package.json +145 -0
  92. package/src/client/digitalocean-app-deploy.client.ts +226 -0
  93. package/src/client/index.ts +24 -0
  94. package/src/cloud/base.ts +149 -0
  95. package/src/cloud/digitalocean.ts +363 -0
  96. package/src/cloud/factory.ts +190 -0
  97. package/src/cloud/index.ts +81 -0
  98. package/src/doks.plugin.ts +401 -0
  99. package/src/implementations/deployment.impl.ts +93 -0
  100. package/src/implementations/droplet.impl.ts +157 -0
  101. package/src/implementations/gitea.impl.ts +319 -0
  102. package/src/implementations/managed-db.impl.ts +37 -0
  103. package/src/implementations/managed-redis.impl.ts +49 -0
  104. package/src/implementations/spaces.impl.ts +52 -0
  105. package/src/implementations/statefulset.impl.ts +186 -0
  106. package/src/implementations/verdaccio.impl.ts +300 -0
  107. package/src/index.ts +136 -0
  108. package/src/kubernetes/index.ts +754 -0
  109. package/src/secrets/index.ts +9 -0
  110. package/src/secrets/vault.descriptor.ts +28 -0
  111. package/src/secrets/vault.impl.cloud.ts +278 -0
  112. package/src/secrets/vault.impl.ts +149 -0
  113. 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';