@geekmidas/cli 1.4.0 → 1.5.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/CHANGELOG.md +12 -0
- package/dist/{HostingerProvider-B9N-TKbp.mjs → HostingerProvider-402UdK89.mjs} +34 -1
- package/dist/HostingerProvider-402UdK89.mjs.map +1 -0
- package/dist/{HostingerProvider-DUV9-Tzg.cjs → HostingerProvider-BiXdHjiq.cjs} +34 -1
- package/dist/HostingerProvider-BiXdHjiq.cjs.map +1 -0
- package/dist/{Route53Provider-DOWmFnwN.mjs → Route53Provider-DbBo7Uz5.mjs} +55 -2
- package/dist/Route53Provider-DbBo7Uz5.mjs.map +1 -0
- package/dist/{Route53Provider-xrWuBXih.cjs → Route53Provider-kfJ77LmL.cjs} +55 -2
- package/dist/Route53Provider-kfJ77LmL.cjs.map +1 -0
- package/dist/backup-provisioner-B5e-F6zX.cjs +164 -0
- package/dist/backup-provisioner-B5e-F6zX.cjs.map +1 -0
- package/dist/backup-provisioner-BIArpmTr.mjs +163 -0
- package/dist/backup-provisioner-BIArpmTr.mjs.map +1 -0
- package/dist/{config-C1dM7aZb.cjs → config-BYn5yUt5.cjs} +2 -2
- package/dist/{config-C1dM7aZb.cjs.map → config-BYn5yUt5.cjs.map} +1 -1
- package/dist/{config-C1bidhvG.mjs → config-dLNQIvDR.mjs} +2 -2
- package/dist/{config-C1bidhvG.mjs.map → config-dLNQIvDR.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{dokploy-api-z0833e7r.mjs → dokploy-api-2ldYoN3i.mjs} +131 -1
- package/dist/dokploy-api-2ldYoN3i.mjs.map +1 -0
- package/dist/dokploy-api-C93pveuy.mjs +3 -0
- package/dist/dokploy-api-CbDh4o93.cjs +3 -0
- package/dist/{dokploy-api-CQvhV6Hd.cjs → dokploy-api-DLgvEQlr.cjs} +131 -1
- package/dist/dokploy-api-DLgvEQlr.cjs.map +1 -0
- package/dist/{index-DzmZ6SUW.d.cts → index-Ba21_lNt.d.cts} +157 -29
- package/dist/index-Ba21_lNt.d.cts.map +1 -0
- package/dist/{index-DvpWzLD7.d.mts → index-Bj5VNxEL.d.mts} +158 -30
- package/dist/index-Bj5VNxEL.d.mts.map +1 -0
- package/dist/index.cjs +219 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +219 -68
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-9k6a6VA4.mjs → openapi-CMTyaIJJ.mjs} +2 -2
- package/dist/{openapi-9k6a6VA4.mjs.map → openapi-CMTyaIJJ.mjs.map} +1 -1
- package/dist/{openapi-Dcja4e1C.cjs → openapi-CqblwJZ4.cjs} +2 -2
- package/dist/{openapi-Dcja4e1C.cjs.map → openapi-CqblwJZ4.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-B9UZ7fOG.d.mts → types-CZg5iUgD.d.mts} +1 -1
- package/dist/{types-B9UZ7fOG.d.mts.map → types-CZg5iUgD.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CeFgIDC-.cjs → workspace-DIMnYaYt.cjs} +20 -2
- package/dist/{workspace-CeFgIDC-.cjs.map → workspace-DIMnYaYt.cjs.map} +1 -1
- package/dist/{workspace-Cb_I7oCJ.mjs → workspace-Dy8k7Wru.mjs} +20 -2
- package/dist/{workspace-Cb_I7oCJ.mjs.map → workspace-Dy8k7Wru.mjs.map} +1 -1
- package/examples/cron-example.ts +6 -6
- package/examples/function-example.ts +1 -1
- package/package.json +7 -5
- package/src/deploy/__tests__/Route53Provider.spec.ts +23 -0
- package/src/deploy/__tests__/backup-provisioner.spec.ts +428 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +23 -0
- package/src/deploy/__tests__/env-resolver.spec.ts +239 -0
- package/src/deploy/__tests__/sniffer.spec.ts +104 -93
- package/src/deploy/__tests__/undeploy.spec.ts +758 -0
- package/src/deploy/backup-provisioner.ts +316 -0
- package/src/deploy/dns/DnsProvider.ts +39 -1
- package/src/deploy/dns/HostingerProvider.ts +74 -0
- package/src/deploy/dns/Route53Provider.ts +85 -1
- package/src/deploy/dns/index.ts +25 -0
- package/src/deploy/dokploy-api.ts +237 -0
- package/src/deploy/env-resolver.ts +11 -1
- package/src/deploy/index.ts +143 -37
- package/src/deploy/sniffer.ts +39 -7
- package/src/deploy/state.ts +171 -0
- package/src/deploy/undeploy.ts +407 -0
- package/src/generators/FunctionGenerator.ts +1 -1
- package/src/init/generators/monorepo.ts +4 -0
- package/src/init/generators/web.ts +45 -2
- package/src/init/versions.ts +2 -2
- package/src/workspace/schema.ts +34 -0
- package/src/workspace/types.ts +37 -37
- package/dist/HostingerProvider-B9N-TKbp.mjs.map +0 -1
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +0 -1
- package/dist/Route53Provider-DOWmFnwN.mjs.map +0 -1
- package/dist/Route53Provider-xrWuBXih.cjs.map +0 -1
- package/dist/dokploy-api-CQvhV6Hd.cjs.map +0 -1
- package/dist/dokploy-api-CWc02yyg.cjs +0 -3
- package/dist/dokploy-api-DSJYNx88.mjs +0 -3
- package/dist/dokploy-api-z0833e7r.mjs.map +0 -1
- package/dist/index-DvpWzLD7.d.mts.map +0 -1
- package/dist/index-DzmZ6SUW.d.cts.map +0 -1
package/examples/cron-example.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c } from '@geekmidas/constructs/crons';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Example cron that generates a daily report at 9 AM UTC
|
|
5
5
|
*/
|
|
6
|
-
export const dailyReport =
|
|
6
|
+
export const dailyReport = c
|
|
7
7
|
.schedule('cron(0 9 * * ? *)')
|
|
8
8
|
.timeout(600000) // 10 minutes
|
|
9
|
-
.handle(async ({
|
|
9
|
+
.handle(async ({ logger }) => {
|
|
10
10
|
logger.info('Generating daily report');
|
|
11
11
|
|
|
12
12
|
const reportDate = new Date().toISOString().split('T')[0];
|
|
@@ -22,7 +22,7 @@ export const dailyReport = cron
|
|
|
22
22
|
],
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
logger.info('Daily report generated'
|
|
25
|
+
logger.info(reportData, 'Daily report generated');
|
|
26
26
|
|
|
27
27
|
return reportData;
|
|
28
28
|
});
|
|
@@ -30,10 +30,10 @@ export const dailyReport = cron
|
|
|
30
30
|
/**
|
|
31
31
|
* Example cron that runs every hour
|
|
32
32
|
*/
|
|
33
|
-
export const hourlyCleanup =
|
|
33
|
+
export const hourlyCleanup = c
|
|
34
34
|
.schedule('rate(1 hour)')
|
|
35
35
|
.timeout(300000) // 5 minutes
|
|
36
|
-
.handle(async ({
|
|
36
|
+
.handle(async ({ logger }) => {
|
|
37
37
|
logger.info('Running hourly cleanup');
|
|
38
38
|
|
|
39
39
|
// Cleanup logic here
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
43
|
+
"@aws-sdk/client-iam": "~3.971.0",
|
|
43
44
|
"@aws-sdk/client-route-53": "~3.971.0",
|
|
45
|
+
"@aws-sdk/client-s3": "~3.971.0",
|
|
44
46
|
"@aws-sdk/client-ssm": "~3.971.0",
|
|
45
47
|
"@aws-sdk/credential-providers": "~3.971.0",
|
|
46
48
|
"chokidar": "~4.0.3",
|
|
@@ -53,11 +55,11 @@
|
|
|
53
55
|
"pg": "~8.17.1",
|
|
54
56
|
"prompts": "~2.4.2",
|
|
55
57
|
"tsx": "~4.20.3",
|
|
56
|
-
"@geekmidas/constructs": "~1.0.
|
|
57
|
-
"@geekmidas/envkit": "~1.0.
|
|
58
|
-
"@geekmidas/
|
|
58
|
+
"@geekmidas/constructs": "~1.0.4",
|
|
59
|
+
"@geekmidas/envkit": "~1.0.1",
|
|
60
|
+
"@geekmidas/logger": "~1.0.0",
|
|
59
61
|
"@geekmidas/schema": "~1.0.0",
|
|
60
|
-
"@geekmidas/
|
|
62
|
+
"@geekmidas/errors": "~1.0.0"
|
|
61
63
|
},
|
|
62
64
|
"devDependencies": {
|
|
63
65
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -399,4 +399,27 @@ describe('Route53Provider', () => {
|
|
|
399
399
|
).rejects.toThrow('No hosted zone found for domain');
|
|
400
400
|
});
|
|
401
401
|
});
|
|
402
|
+
|
|
403
|
+
describe('default region', () => {
|
|
404
|
+
it('should use us-east-1 as default region when none specified', () => {
|
|
405
|
+
// This test verifies the provider can be created without region
|
|
406
|
+
// and doesn't throw "Region is missing" error
|
|
407
|
+
const providerWithoutRegion = new Route53Provider({
|
|
408
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
409
|
+
hostedZoneId: 'test-zone',
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(providerWithoutRegion.name).toBe('route53');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should use provided region when specified', () => {
|
|
416
|
+
const providerWithRegion = new Route53Provider({
|
|
417
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
418
|
+
region: 'eu-west-1',
|
|
419
|
+
hostedZoneId: 'test-zone',
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(providerWithRegion.name).toBe('route53');
|
|
423
|
+
});
|
|
424
|
+
});
|
|
402
425
|
});
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteAccessKeyCommand,
|
|
3
|
+
DeleteUserCommand,
|
|
4
|
+
DeleteUserPolicyCommand,
|
|
5
|
+
GetUserCommand,
|
|
6
|
+
IAMClient,
|
|
7
|
+
ListAccessKeysCommand,
|
|
8
|
+
} from '@aws-sdk/client-iam';
|
|
9
|
+
import {
|
|
10
|
+
DeleteBucketCommand,
|
|
11
|
+
DeleteObjectsCommand,
|
|
12
|
+
ListObjectsV2Command,
|
|
13
|
+
S3Client,
|
|
14
|
+
} from '@aws-sdk/client-s3';
|
|
15
|
+
import { HttpResponse, http } from 'msw';
|
|
16
|
+
import { setupServer } from 'msw/node';
|
|
17
|
+
import {
|
|
18
|
+
afterAll,
|
|
19
|
+
afterEach,
|
|
20
|
+
beforeAll,
|
|
21
|
+
beforeEach,
|
|
22
|
+
describe,
|
|
23
|
+
expect,
|
|
24
|
+
it,
|
|
25
|
+
} from 'vitest';
|
|
26
|
+
import {
|
|
27
|
+
type ProvisionBackupOptions,
|
|
28
|
+
provisionBackupDestination,
|
|
29
|
+
} from '../backup-provisioner';
|
|
30
|
+
import { DokployApi } from '../dokploy-api';
|
|
31
|
+
import type { BackupState } from '../state';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Backup Provisioner Tests
|
|
35
|
+
*
|
|
36
|
+
* These tests require LocalStack to be running with S3 and IAM enabled.
|
|
37
|
+
* Run: docker compose up -d localstack
|
|
38
|
+
*/
|
|
39
|
+
describe('backup-provisioner', () => {
|
|
40
|
+
const LOCALSTACK_ENDPOINT = 'http://localhost:4566';
|
|
41
|
+
const DOKPLOY_BASE_URL = 'https://dokploy.example.com';
|
|
42
|
+
|
|
43
|
+
let s3Client: S3Client;
|
|
44
|
+
let iamClient: IAMClient;
|
|
45
|
+
let dokployApi: DokployApi;
|
|
46
|
+
|
|
47
|
+
const server = setupServer();
|
|
48
|
+
const logs: string[] = [];
|
|
49
|
+
const logger = { log: (msg: string) => logs.push(msg) };
|
|
50
|
+
|
|
51
|
+
// Track created resources for cleanup
|
|
52
|
+
const createdBuckets: string[] = [];
|
|
53
|
+
const createdUsers: string[] = [];
|
|
54
|
+
|
|
55
|
+
beforeAll(() => {
|
|
56
|
+
process.env.AWS_ACCESS_KEY_ID = 'test';
|
|
57
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'test';
|
|
58
|
+
server.listen({ onUnhandledRequest: 'bypass' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
logs.length = 0;
|
|
63
|
+
|
|
64
|
+
s3Client = new S3Client({
|
|
65
|
+
region: 'us-east-1',
|
|
66
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
67
|
+
forcePathStyle: true,
|
|
68
|
+
credentials: {
|
|
69
|
+
accessKeyId: 'test',
|
|
70
|
+
secretAccessKey: 'test',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
iamClient = new IAMClient({
|
|
75
|
+
region: 'us-east-1',
|
|
76
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
77
|
+
credentials: {
|
|
78
|
+
accessKeyId: 'test',
|
|
79
|
+
secretAccessKey: 'test',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
dokployApi = new DokployApi({
|
|
84
|
+
baseUrl: DOKPLOY_BASE_URL,
|
|
85
|
+
token: 'test-api-token',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(async () => {
|
|
90
|
+
server.resetHandlers();
|
|
91
|
+
|
|
92
|
+
// Cleanup created buckets
|
|
93
|
+
for (const bucket of createdBuckets) {
|
|
94
|
+
try {
|
|
95
|
+
// First delete all objects in the bucket
|
|
96
|
+
const objects = await s3Client.send(
|
|
97
|
+
new ListObjectsV2Command({ Bucket: bucket }),
|
|
98
|
+
);
|
|
99
|
+
if (objects.Contents?.length) {
|
|
100
|
+
await s3Client.send(
|
|
101
|
+
new DeleteObjectsCommand({
|
|
102
|
+
Bucket: bucket,
|
|
103
|
+
Delete: {
|
|
104
|
+
Objects: objects.Contents.map((o) => ({ Key: o.Key })),
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
await s3Client.send(new DeleteBucketCommand({ Bucket: bucket }));
|
|
110
|
+
} catch {
|
|
111
|
+
// Ignore cleanup errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
createdBuckets.length = 0;
|
|
115
|
+
|
|
116
|
+
// Cleanup created IAM users
|
|
117
|
+
for (const userName of createdUsers) {
|
|
118
|
+
try {
|
|
119
|
+
// Delete access keys first
|
|
120
|
+
const keys = await iamClient.send(
|
|
121
|
+
new ListAccessKeysCommand({ UserName: userName }),
|
|
122
|
+
);
|
|
123
|
+
for (const key of keys.AccessKeyMetadata ?? []) {
|
|
124
|
+
await iamClient.send(
|
|
125
|
+
new DeleteAccessKeyCommand({
|
|
126
|
+
UserName: userName,
|
|
127
|
+
AccessKeyId: key.AccessKeyId,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
// Delete user policy
|
|
132
|
+
await iamClient.send(
|
|
133
|
+
new DeleteUserPolicyCommand({
|
|
134
|
+
UserName: userName,
|
|
135
|
+
PolicyName: 'DokployBackupAccess',
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
// Delete user
|
|
139
|
+
await iamClient.send(new DeleteUserCommand({ UserName: userName }));
|
|
140
|
+
} catch {
|
|
141
|
+
// Ignore cleanup errors
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
createdUsers.length = 0;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterAll(() => {
|
|
148
|
+
server.close();
|
|
149
|
+
s3Client.destroy();
|
|
150
|
+
iamClient.destroy();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Helper to create provision options with LocalStack clients
|
|
155
|
+
*/
|
|
156
|
+
function createOptions(
|
|
157
|
+
overrides: Partial<ProvisionBackupOptions> = {},
|
|
158
|
+
): ProvisionBackupOptions {
|
|
159
|
+
return {
|
|
160
|
+
api: dokployApi,
|
|
161
|
+
projectId: 'proj_test123',
|
|
162
|
+
projectName: 'test-project',
|
|
163
|
+
stage: 'production',
|
|
164
|
+
config: {
|
|
165
|
+
type: 's3',
|
|
166
|
+
region: 'us-east-1',
|
|
167
|
+
},
|
|
168
|
+
logger,
|
|
169
|
+
awsEndpoint: LOCALSTACK_ENDPOINT,
|
|
170
|
+
...overrides,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Setup mock Dokploy API responses
|
|
176
|
+
*/
|
|
177
|
+
function setupDokployMocks(options: {
|
|
178
|
+
existingDestination?: { destinationId: string; name: string };
|
|
179
|
+
createDestinationId?: string;
|
|
180
|
+
}) {
|
|
181
|
+
const handlers = [
|
|
182
|
+
// List destinations
|
|
183
|
+
http.get(`${DOKPLOY_BASE_URL}/api/destination.all`, () => {
|
|
184
|
+
if (options.existingDestination) {
|
|
185
|
+
return HttpResponse.json([options.existingDestination]);
|
|
186
|
+
}
|
|
187
|
+
return HttpResponse.json([]);
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
// Get destination
|
|
191
|
+
http.get(`${DOKPLOY_BASE_URL}/api/destination.one`, ({ request }) => {
|
|
192
|
+
const url = new URL(request.url);
|
|
193
|
+
const destId = url.searchParams.get('destinationId');
|
|
194
|
+
if (
|
|
195
|
+
options.existingDestination &&
|
|
196
|
+
destId === options.existingDestination.destinationId
|
|
197
|
+
) {
|
|
198
|
+
return HttpResponse.json(options.existingDestination);
|
|
199
|
+
}
|
|
200
|
+
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
|
201
|
+
}),
|
|
202
|
+
|
|
203
|
+
// Create destination
|
|
204
|
+
http.post(`${DOKPLOY_BASE_URL}/api/destination.create`, () => {
|
|
205
|
+
return HttpResponse.json({
|
|
206
|
+
destinationId: options.createDestinationId ?? 'dest_new123',
|
|
207
|
+
name: 'test-project-production-s3',
|
|
208
|
+
});
|
|
209
|
+
}),
|
|
210
|
+
|
|
211
|
+
// Test destination connection
|
|
212
|
+
http.post(`${DOKPLOY_BASE_URL}/api/destination.testConnection`, () => {
|
|
213
|
+
return HttpResponse.json({ success: true });
|
|
214
|
+
}),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
server.use(...handlers);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
describe('provisionBackupDestination', () => {
|
|
221
|
+
it('should create S3 bucket with unique name', async () => {
|
|
222
|
+
setupDokployMocks({ createDestinationId: 'dest_123' });
|
|
223
|
+
|
|
224
|
+
const options = createOptions();
|
|
225
|
+
const result = await provisionBackupDestination(options);
|
|
226
|
+
|
|
227
|
+
// Track for cleanup
|
|
228
|
+
createdBuckets.push(result.bucketName);
|
|
229
|
+
createdUsers.push(result.iamUserName);
|
|
230
|
+
|
|
231
|
+
expect(result.bucketName).toMatch(/^test-project-production-backups-/);
|
|
232
|
+
expect(result.bucketArn).toBe(`arn:aws:s3:::${result.bucketName}`);
|
|
233
|
+
expect(logs.some((l) => l.includes('Creating S3 bucket'))).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should create IAM user with correct name', async () => {
|
|
237
|
+
setupDokployMocks({ createDestinationId: 'dest_123' });
|
|
238
|
+
|
|
239
|
+
const options = createOptions();
|
|
240
|
+
const result = await provisionBackupDestination(options);
|
|
241
|
+
|
|
242
|
+
// Track for cleanup
|
|
243
|
+
createdBuckets.push(result.bucketName);
|
|
244
|
+
createdUsers.push(result.iamUserName);
|
|
245
|
+
|
|
246
|
+
expect(result.iamUserName).toBe('dokploy-backup-test-project-production');
|
|
247
|
+
|
|
248
|
+
// Verify user exists in IAM
|
|
249
|
+
const userResponse = await iamClient.send(
|
|
250
|
+
new GetUserCommand({ UserName: result.iamUserName }),
|
|
251
|
+
);
|
|
252
|
+
expect(userResponse.User?.UserName).toBe(result.iamUserName);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should create IAM access key', async () => {
|
|
256
|
+
setupDokployMocks({ createDestinationId: 'dest_123' });
|
|
257
|
+
|
|
258
|
+
const options = createOptions();
|
|
259
|
+
const result = await provisionBackupDestination(options);
|
|
260
|
+
|
|
261
|
+
// Track for cleanup
|
|
262
|
+
createdBuckets.push(result.bucketName);
|
|
263
|
+
createdUsers.push(result.iamUserName);
|
|
264
|
+
|
|
265
|
+
expect(result.iamAccessKeyId).toBeDefined();
|
|
266
|
+
expect(result.iamSecretAccessKey).toBeDefined();
|
|
267
|
+
expect(result.iamAccessKeyId.length).toBeGreaterThan(0);
|
|
268
|
+
expect(result.iamSecretAccessKey.length).toBeGreaterThan(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should create Dokploy destination', async () => {
|
|
272
|
+
setupDokployMocks({ createDestinationId: 'dest_created' });
|
|
273
|
+
|
|
274
|
+
const options = createOptions();
|
|
275
|
+
const result = await provisionBackupDestination(options);
|
|
276
|
+
|
|
277
|
+
// Track for cleanup
|
|
278
|
+
createdBuckets.push(result.bucketName);
|
|
279
|
+
createdUsers.push(result.iamUserName);
|
|
280
|
+
|
|
281
|
+
expect(result.destinationId).toBe('dest_created');
|
|
282
|
+
expect(logs.some((l) => l.includes('Dokploy destination created'))).toBe(
|
|
283
|
+
true,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should reuse existing state when destination still exists', async () => {
|
|
288
|
+
const existingState: BackupState = {
|
|
289
|
+
bucketName: 'existing-bucket',
|
|
290
|
+
bucketArn: 'arn:aws:s3:::existing-bucket',
|
|
291
|
+
iamUserName: 'existing-user',
|
|
292
|
+
iamAccessKeyId: 'AKIA123',
|
|
293
|
+
iamSecretAccessKey: 'secret123',
|
|
294
|
+
destinationId: 'dest_existing',
|
|
295
|
+
region: 'us-east-1',
|
|
296
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
setupDokployMocks({
|
|
300
|
+
existingDestination: {
|
|
301
|
+
destinationId: 'dest_existing',
|
|
302
|
+
name: 'existing',
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const options = createOptions({ existingState });
|
|
307
|
+
const result = await provisionBackupDestination(options);
|
|
308
|
+
|
|
309
|
+
// Should return existing state without creating new resources
|
|
310
|
+
expect(result).toEqual(existingState);
|
|
311
|
+
expect(
|
|
312
|
+
logs.some((l) => l.includes('Using existing backup destination')),
|
|
313
|
+
).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should recreate destination if existing one not found', async () => {
|
|
317
|
+
const existingState: BackupState = {
|
|
318
|
+
bucketName: 'existing-bucket',
|
|
319
|
+
bucketArn: 'arn:aws:s3:::existing-bucket',
|
|
320
|
+
iamUserName: 'dokploy-backup-test-project-production',
|
|
321
|
+
iamAccessKeyId: 'AKIA123',
|
|
322
|
+
iamSecretAccessKey: 'secret123',
|
|
323
|
+
destinationId: 'dest_deleted',
|
|
324
|
+
region: 'us-east-1',
|
|
325
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Mock: destination.one returns 404, meaning destination was deleted
|
|
329
|
+
setupDokployMocks({ createDestinationId: 'dest_new' });
|
|
330
|
+
|
|
331
|
+
const options = createOptions({ existingState });
|
|
332
|
+
const result = await provisionBackupDestination(options);
|
|
333
|
+
|
|
334
|
+
// Track for cleanup (uses existing bucket name from state)
|
|
335
|
+
createdBuckets.push(result.bucketName);
|
|
336
|
+
createdUsers.push(result.iamUserName);
|
|
337
|
+
|
|
338
|
+
// Should reuse bucket name from state
|
|
339
|
+
expect(result.bucketName).toBe('existing-bucket');
|
|
340
|
+
// Should reuse IAM user name
|
|
341
|
+
expect(result.iamUserName).toBe('dokploy-backup-test-project-production');
|
|
342
|
+
// Should reuse credentials from state
|
|
343
|
+
expect(result.iamAccessKeyId).toBe('AKIA123');
|
|
344
|
+
// Should create new destination
|
|
345
|
+
expect(result.destinationId).toBe('dest_new');
|
|
346
|
+
expect(
|
|
347
|
+
logs.some((l) => l.includes('Existing destination not found')),
|
|
348
|
+
).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should sanitize project name for AWS resources', async () => {
|
|
352
|
+
setupDokployMocks({ createDestinationId: 'dest_123' });
|
|
353
|
+
|
|
354
|
+
const options = createOptions({
|
|
355
|
+
projectName: 'My Project_Name!@#',
|
|
356
|
+
});
|
|
357
|
+
const result = await provisionBackupDestination(options);
|
|
358
|
+
|
|
359
|
+
// Track for cleanup
|
|
360
|
+
createdBuckets.push(result.bucketName);
|
|
361
|
+
createdUsers.push(result.iamUserName);
|
|
362
|
+
|
|
363
|
+
// Should be lowercase with only alphanumeric and hyphens
|
|
364
|
+
// 'My Project_Name!@#' -> 'my-project-name----' (space, underscore, !, @, # all become hyphens)
|
|
365
|
+
expect(result.bucketName).toMatch(
|
|
366
|
+
/^my-project-name----production-backups-/,
|
|
367
|
+
);
|
|
368
|
+
expect(result.iamUserName).toBe(
|
|
369
|
+
'dokploy-backup-my-project-name----production',
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should return complete BackupState', async () => {
|
|
374
|
+
setupDokployMocks({ createDestinationId: 'dest_complete' });
|
|
375
|
+
|
|
376
|
+
const options = createOptions();
|
|
377
|
+
const result = await provisionBackupDestination(options);
|
|
378
|
+
|
|
379
|
+
// Track for cleanup
|
|
380
|
+
createdBuckets.push(result.bucketName);
|
|
381
|
+
createdUsers.push(result.iamUserName);
|
|
382
|
+
|
|
383
|
+
expect(result).toMatchObject({
|
|
384
|
+
bucketName: expect.stringMatching(/^test-project-production-backups-/),
|
|
385
|
+
bucketArn: expect.stringMatching(/^arn:aws:s3:::/),
|
|
386
|
+
iamUserName: 'dokploy-backup-test-project-production',
|
|
387
|
+
iamAccessKeyId: expect.any(String),
|
|
388
|
+
iamSecretAccessKey: expect.any(String),
|
|
389
|
+
destinationId: 'dest_complete',
|
|
390
|
+
region: 'us-east-1',
|
|
391
|
+
createdAt: expect.any(String),
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should handle connection test failure gracefully', async () => {
|
|
396
|
+
server.use(
|
|
397
|
+
http.get(`${DOKPLOY_BASE_URL}/api/destination.all`, () => {
|
|
398
|
+
return HttpResponse.json([]);
|
|
399
|
+
}),
|
|
400
|
+
http.post(`${DOKPLOY_BASE_URL}/api/destination.create`, () => {
|
|
401
|
+
return HttpResponse.json({
|
|
402
|
+
destinationId: 'dest_123',
|
|
403
|
+
name: 'test',
|
|
404
|
+
});
|
|
405
|
+
}),
|
|
406
|
+
http.post(`${DOKPLOY_BASE_URL}/api/destination.testConnection`, () => {
|
|
407
|
+
return HttpResponse.json(
|
|
408
|
+
{ message: 'Connection failed' },
|
|
409
|
+
{ status: 500 },
|
|
410
|
+
);
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const options = createOptions();
|
|
415
|
+
const result = await provisionBackupDestination(options);
|
|
416
|
+
|
|
417
|
+
// Track for cleanup
|
|
418
|
+
createdBuckets.push(result.bucketName);
|
|
419
|
+
createdUsers.push(result.iamUserName);
|
|
420
|
+
|
|
421
|
+
// Should still succeed but log warning
|
|
422
|
+
expect(result.destinationId).toBe('dest_123');
|
|
423
|
+
expect(
|
|
424
|
+
logs.some((l) => l.includes('Warning: Could not verify destination')),
|
|
425
|
+
).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
createDnsProvider,
|
|
4
|
+
type DeleteDnsRecord,
|
|
5
|
+
type DeleteResult,
|
|
4
6
|
type DnsProvider,
|
|
5
7
|
type DnsRecord,
|
|
6
8
|
isDnsProvider,
|
|
@@ -14,6 +16,7 @@ describe('isDnsProvider', () => {
|
|
|
14
16
|
name: 'test',
|
|
15
17
|
getRecords: async () => [],
|
|
16
18
|
upsertRecords: async () => [],
|
|
19
|
+
deleteRecords: async () => [],
|
|
17
20
|
};
|
|
18
21
|
expect(isDnsProvider(provider)).toBe(true);
|
|
19
22
|
});
|
|
@@ -126,6 +129,16 @@ describe('createDnsProvider', () => {
|
|
|
126
129
|
async upsertRecords(): Promise<UpsertResult[]> {
|
|
127
130
|
return [];
|
|
128
131
|
},
|
|
132
|
+
async deleteRecords(
|
|
133
|
+
_domain: string,
|
|
134
|
+
records: DeleteDnsRecord[],
|
|
135
|
+
): Promise<DeleteResult[]> {
|
|
136
|
+
return records.map((r) => ({
|
|
137
|
+
record: r,
|
|
138
|
+
deleted: true,
|
|
139
|
+
notFound: false,
|
|
140
|
+
}));
|
|
141
|
+
},
|
|
129
142
|
};
|
|
130
143
|
|
|
131
144
|
const provider = await createDnsProvider({
|
|
@@ -157,6 +170,16 @@ describe('createDnsProvider', () => {
|
|
|
157
170
|
unchanged: false,
|
|
158
171
|
}));
|
|
159
172
|
},
|
|
173
|
+
async deleteRecords(
|
|
174
|
+
_domain: string,
|
|
175
|
+
records: DeleteDnsRecord[],
|
|
176
|
+
): Promise<DeleteResult[]> {
|
|
177
|
+
return records.map((r) => ({
|
|
178
|
+
record: r,
|
|
179
|
+
deleted: true,
|
|
180
|
+
notFound: false,
|
|
181
|
+
}));
|
|
182
|
+
},
|
|
160
183
|
};
|
|
161
184
|
|
|
162
185
|
const provider = await createDnsProvider({
|