@geekmidas/cli 1.5.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 +6 -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-C8mS0zY6.mjs → Route53Provider-DbBo7Uz5.mjs} +53 -1
- package/dist/Route53Provider-DbBo7Uz5.mjs.map +1 -0
- package/dist/{Route53Provider-Bs7Arms9.cjs → Route53Provider-kfJ77LmL.cjs} +53 -1
- 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-ZQM1vBoz.cjs → config-BYn5yUt5.cjs} +2 -2
- package/dist/{config-ZQM1vBoz.cjs.map → config-BYn5yUt5.cjs.map} +1 -1
- package/dist/{config-DfCJ29PQ.mjs → config-dLNQIvDR.mjs} +2 -2
- package/dist/{config-DfCJ29PQ.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-B58qjyBd.d.cts → index-Ba21_lNt.d.cts} +131 -29
- package/dist/index-Ba21_lNt.d.cts.map +1 -0
- package/dist/{index-C0SpUT9Y.d.mts → index-Bj5VNxEL.d.mts} +132 -30
- package/dist/index-Bj5VNxEL.d.mts.map +1 -0
- package/dist/index.cjs +119 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +119 -25
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BcSjLfWq.mjs → openapi-CMTyaIJJ.mjs} +2 -2
- package/dist/{openapi-BcSjLfWq.mjs.map → openapi-CMTyaIJJ.mjs.map} +1 -1
- package/dist/{openapi-D6Hcfov0.cjs → openapi-CqblwJZ4.cjs} +2 -2
- package/dist/{openapi-D6Hcfov0.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-2Do2YcGZ.cjs → workspace-DIMnYaYt.cjs} +16 -2
- package/dist/{workspace-2Do2YcGZ.cjs.map → workspace-DIMnYaYt.cjs.map} +1 -1
- package/dist/{workspace-BW2iU37P.mjs → workspace-Dy8k7Wru.mjs} +16 -2
- package/dist/{workspace-BW2iU37P.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__/backup-provisioner.spec.ts +428 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +23 -0
- package/src/deploy/__tests__/env-resolver.spec.ts +1 -1
- 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 +81 -0
- package/src/deploy/dns/index.ts +25 -0
- package/src/deploy/dokploy-api.ts +237 -0
- package/src/deploy/index.ts +71 -13
- package/src/deploy/state.ts +171 -0
- package/src/deploy/undeploy.ts +407 -0
- package/src/generators/FunctionGenerator.ts +1 -1
- package/src/init/versions.ts +2 -2
- package/src/workspace/schema.ts +26 -0
- package/src/workspace/types.ts +14 -37
- package/dist/HostingerProvider-B9N-TKbp.mjs.map +0 -1
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +0 -1
- package/dist/Route53Provider-Bs7Arms9.cjs.map +0 -1
- package/dist/Route53Provider-C8mS0zY6.mjs.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-B58qjyBd.d.cts.map +0 -1
- package/dist/index-C0SpUT9Y.d.mts.map +0 -1
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { HttpResponse, http } from 'msw';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import {
|
|
4
|
+
afterAll,
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeAll,
|
|
7
|
+
beforeEach,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
it,
|
|
11
|
+
} from 'vitest';
|
|
12
|
+
import type {
|
|
13
|
+
DeleteDnsRecord,
|
|
14
|
+
DeleteResult,
|
|
15
|
+
DnsProvider,
|
|
16
|
+
DnsRecord,
|
|
17
|
+
UpsertDnsRecord,
|
|
18
|
+
UpsertResult,
|
|
19
|
+
} from '../dns/DnsProvider';
|
|
20
|
+
import { DokployApi } from '../dokploy-api';
|
|
21
|
+
import type { DokployStageState } from '../state';
|
|
22
|
+
import { type UndeployOptions, undeploy } from '../undeploy';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Undeploy Tests
|
|
26
|
+
*/
|
|
27
|
+
describe('undeploy', () => {
|
|
28
|
+
const DOKPLOY_BASE_URL = 'https://dokploy.example.com';
|
|
29
|
+
|
|
30
|
+
let dokployApi: DokployApi;
|
|
31
|
+
const server = setupServer();
|
|
32
|
+
const logs: string[] = [];
|
|
33
|
+
const logger = { log: (msg: string) => logs.push(msg) };
|
|
34
|
+
|
|
35
|
+
// Track which endpoints were called
|
|
36
|
+
const calledEndpoints: string[] = [];
|
|
37
|
+
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
server.listen({ onUnhandledRequest: 'bypass' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
logs.length = 0;
|
|
44
|
+
calledEndpoints.length = 0;
|
|
45
|
+
|
|
46
|
+
dokployApi = new DokployApi({
|
|
47
|
+
baseUrl: DOKPLOY_BASE_URL,
|
|
48
|
+
token: 'test-api-token',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
server.resetHandlers();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
server.close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function createState(
|
|
61
|
+
overrides: Partial<DokployStageState> = {},
|
|
62
|
+
): DokployStageState {
|
|
63
|
+
return {
|
|
64
|
+
provider: 'dokploy',
|
|
65
|
+
stage: 'production',
|
|
66
|
+
projectId: 'proj_123',
|
|
67
|
+
environmentId: 'env_123',
|
|
68
|
+
applications: {},
|
|
69
|
+
services: {},
|
|
70
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createOptions(
|
|
76
|
+
state: DokployStageState,
|
|
77
|
+
overrides: Partial<UndeployOptions> = {},
|
|
78
|
+
): UndeployOptions {
|
|
79
|
+
return {
|
|
80
|
+
api: dokployApi,
|
|
81
|
+
state,
|
|
82
|
+
logger,
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Mock DNS provider for testing
|
|
88
|
+
function createMockDnsProvider(
|
|
89
|
+
deleteResults: DeleteResult[] = [],
|
|
90
|
+
): DnsProvider & { deletedRecords: DeleteDnsRecord[] } {
|
|
91
|
+
const provider = {
|
|
92
|
+
name: 'mock',
|
|
93
|
+
deletedRecords: [] as DeleteDnsRecord[],
|
|
94
|
+
async getRecords(_domain: string): Promise<DnsRecord[]> {
|
|
95
|
+
return [];
|
|
96
|
+
},
|
|
97
|
+
async upsertRecords(
|
|
98
|
+
_domain: string,
|
|
99
|
+
_records: UpsertDnsRecord[],
|
|
100
|
+
): Promise<UpsertResult[]> {
|
|
101
|
+
return [];
|
|
102
|
+
},
|
|
103
|
+
async deleteRecords(
|
|
104
|
+
_domain: string,
|
|
105
|
+
records: DeleteDnsRecord[],
|
|
106
|
+
): Promise<DeleteResult[]> {
|
|
107
|
+
provider.deletedRecords.push(...records);
|
|
108
|
+
if (deleteResults.length > 0) {
|
|
109
|
+
return deleteResults;
|
|
110
|
+
}
|
|
111
|
+
return records.map((r) => ({
|
|
112
|
+
record: r,
|
|
113
|
+
deleted: true,
|
|
114
|
+
notFound: false,
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
return provider;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setupMocks() {
|
|
122
|
+
server.use(
|
|
123
|
+
// Application remove
|
|
124
|
+
http.post(
|
|
125
|
+
`${DOKPLOY_BASE_URL}/api/application.remove`,
|
|
126
|
+
async ({ request }) => {
|
|
127
|
+
const body = (await request.json()) as { applicationId: string };
|
|
128
|
+
calledEndpoints.push(`application.remove:${body.applicationId}`);
|
|
129
|
+
return HttpResponse.json({ success: true });
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
|
|
133
|
+
// Postgres remove
|
|
134
|
+
http.post(
|
|
135
|
+
`${DOKPLOY_BASE_URL}/api/postgres.remove`,
|
|
136
|
+
async ({ request }) => {
|
|
137
|
+
const body = (await request.json()) as { postgresId: string };
|
|
138
|
+
calledEndpoints.push(`postgres.remove:${body.postgresId}`);
|
|
139
|
+
return HttpResponse.json({ success: true });
|
|
140
|
+
},
|
|
141
|
+
),
|
|
142
|
+
|
|
143
|
+
// Redis remove
|
|
144
|
+
http.post(`${DOKPLOY_BASE_URL}/api/redis.remove`, async ({ request }) => {
|
|
145
|
+
const body = (await request.json()) as { redisId: string };
|
|
146
|
+
calledEndpoints.push(`redis.remove:${body.redisId}`);
|
|
147
|
+
return HttpResponse.json({ success: true });
|
|
148
|
+
}),
|
|
149
|
+
|
|
150
|
+
// Project remove
|
|
151
|
+
http.post(
|
|
152
|
+
`${DOKPLOY_BASE_URL}/api/project.remove`,
|
|
153
|
+
async ({ request }) => {
|
|
154
|
+
const body = (await request.json()) as { projectId: string };
|
|
155
|
+
calledEndpoints.push(`project.remove:${body.projectId}`);
|
|
156
|
+
return HttpResponse.json({ success: true });
|
|
157
|
+
},
|
|
158
|
+
),
|
|
159
|
+
|
|
160
|
+
// Destination remove
|
|
161
|
+
http.post(
|
|
162
|
+
`${DOKPLOY_BASE_URL}/api/destination.remove`,
|
|
163
|
+
async ({ request }) => {
|
|
164
|
+
const body = (await request.json()) as { destinationId: string };
|
|
165
|
+
calledEndpoints.push(`destination.remove:${body.destinationId}`);
|
|
166
|
+
return HttpResponse.json({ success: true });
|
|
167
|
+
},
|
|
168
|
+
),
|
|
169
|
+
|
|
170
|
+
// Backup remove
|
|
171
|
+
http.post(
|
|
172
|
+
`${DOKPLOY_BASE_URL}/api/backup.remove`,
|
|
173
|
+
async ({ request }) => {
|
|
174
|
+
const body = (await request.json()) as { backupId: string };
|
|
175
|
+
calledEndpoints.push(`backup.remove:${body.backupId}`);
|
|
176
|
+
return HttpResponse.json({ success: true });
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
|
|
180
|
+
// Manual backup
|
|
181
|
+
http.post(
|
|
182
|
+
`${DOKPLOY_BASE_URL}/api/backup.manualBackup`,
|
|
183
|
+
async ({ request }) => {
|
|
184
|
+
const body = (await request.json()) as { backupId: string };
|
|
185
|
+
calledEndpoints.push(`backup.manualBackup:${body.backupId}`);
|
|
186
|
+
return HttpResponse.json({ success: true });
|
|
187
|
+
},
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
describe('deleting applications', () => {
|
|
193
|
+
it('should delete all applications', async () => {
|
|
194
|
+
setupMocks();
|
|
195
|
+
|
|
196
|
+
const state = createState({
|
|
197
|
+
applications: {
|
|
198
|
+
api: 'app_api_123',
|
|
199
|
+
web: 'app_web_456',
|
|
200
|
+
worker: 'app_worker_789',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const result = await undeploy(createOptions(state));
|
|
205
|
+
|
|
206
|
+
expect(result.deletedApplications).toEqual(['api', 'web', 'worker']);
|
|
207
|
+
expect(calledEndpoints).toContain('application.remove:app_api_123');
|
|
208
|
+
expect(calledEndpoints).toContain('application.remove:app_web_456');
|
|
209
|
+
expect(calledEndpoints).toContain('application.remove:app_worker_789');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle application deletion errors gracefully', async () => {
|
|
213
|
+
server.use(
|
|
214
|
+
http.post(`${DOKPLOY_BASE_URL}/api/application.remove`, () => {
|
|
215
|
+
return HttpResponse.json(
|
|
216
|
+
{ message: 'Application not found' },
|
|
217
|
+
{ status: 404 },
|
|
218
|
+
);
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const state = createState({
|
|
223
|
+
applications: { api: 'app_missing' },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = await undeploy(createOptions(state));
|
|
227
|
+
|
|
228
|
+
expect(result.deletedApplications).toEqual([]);
|
|
229
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
230
|
+
expect(result.errors[0]).toContain('Failed to delete application api');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('deleting services', () => {
|
|
235
|
+
it('should delete postgres when present', async () => {
|
|
236
|
+
setupMocks();
|
|
237
|
+
|
|
238
|
+
const state = createState({
|
|
239
|
+
services: { postgresId: 'pg_123' },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const result = await undeploy(createOptions(state));
|
|
243
|
+
|
|
244
|
+
expect(result.deletedPostgres).toBe(true);
|
|
245
|
+
expect(calledEndpoints).toContain('postgres.remove:pg_123');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should delete redis when present', async () => {
|
|
249
|
+
setupMocks();
|
|
250
|
+
|
|
251
|
+
const state = createState({
|
|
252
|
+
services: { redisId: 'redis_123' },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const result = await undeploy(createOptions(state));
|
|
256
|
+
|
|
257
|
+
expect(result.deletedRedis).toBe(true);
|
|
258
|
+
expect(calledEndpoints).toContain('redis.remove:redis_123');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should delete both postgres and redis', async () => {
|
|
262
|
+
setupMocks();
|
|
263
|
+
|
|
264
|
+
const state = createState({
|
|
265
|
+
services: {
|
|
266
|
+
postgresId: 'pg_123',
|
|
267
|
+
redisId: 'redis_456',
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const result = await undeploy(createOptions(state));
|
|
272
|
+
|
|
273
|
+
expect(result.deletedPostgres).toBe(true);
|
|
274
|
+
expect(result.deletedRedis).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should handle missing services gracefully', async () => {
|
|
278
|
+
setupMocks();
|
|
279
|
+
|
|
280
|
+
const state = createState({
|
|
281
|
+
services: {},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await undeploy(createOptions(state));
|
|
285
|
+
|
|
286
|
+
expect(result.deletedPostgres).toBe(false);
|
|
287
|
+
expect(result.deletedRedis).toBe(false);
|
|
288
|
+
expect(result.errors).toEqual([]);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('deleting project', () => {
|
|
293
|
+
it('should not delete project by default', async () => {
|
|
294
|
+
setupMocks();
|
|
295
|
+
|
|
296
|
+
const state = createState();
|
|
297
|
+
const result = await undeploy(createOptions(state));
|
|
298
|
+
|
|
299
|
+
expect(result.deletedProject).toBe(false);
|
|
300
|
+
expect(calledEndpoints).not.toContain('project.remove:proj_123');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should delete project when deleteProject is true', async () => {
|
|
304
|
+
setupMocks();
|
|
305
|
+
|
|
306
|
+
const state = createState();
|
|
307
|
+
const result = await undeploy(
|
|
308
|
+
createOptions(state, { deleteProject: true }),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(result.deletedProject).toBe(true);
|
|
312
|
+
expect(calledEndpoints).toContain('project.remove:proj_123');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('deleting backups', () => {
|
|
317
|
+
it('should run manual backup before deleting anything', async () => {
|
|
318
|
+
setupMocks();
|
|
319
|
+
|
|
320
|
+
const state = createState({
|
|
321
|
+
services: { postgresId: 'pg_123' },
|
|
322
|
+
backups: {
|
|
323
|
+
bucketName: 'test-bucket',
|
|
324
|
+
bucketArn: 'arn:aws:s3:::test-bucket',
|
|
325
|
+
iamUserName: 'test-user',
|
|
326
|
+
iamAccessKeyId: 'AKIA00000TEST',
|
|
327
|
+
iamSecretAccessKey: 'secret',
|
|
328
|
+
destinationId: 'dest_123',
|
|
329
|
+
postgresBackupId: 'backup_123',
|
|
330
|
+
region: 'us-east-1',
|
|
331
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await undeploy(createOptions(state));
|
|
336
|
+
|
|
337
|
+
// Verify manual backup is called first before any deletions
|
|
338
|
+
expect(calledEndpoints[0]).toBe('backup.manualBackup:backup_123');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should delete backup schedule before postgres', async () => {
|
|
342
|
+
setupMocks();
|
|
343
|
+
|
|
344
|
+
const state = createState({
|
|
345
|
+
services: { postgresId: 'pg_123' },
|
|
346
|
+
backups: {
|
|
347
|
+
bucketName: 'test-bucket',
|
|
348
|
+
bucketArn: 'arn:aws:s3:::test-bucket',
|
|
349
|
+
iamUserName: 'test-user',
|
|
350
|
+
iamAccessKeyId: 'AKIA00000TEST',
|
|
351
|
+
iamSecretAccessKey: 'secret',
|
|
352
|
+
destinationId: 'dest_123',
|
|
353
|
+
postgresBackupId: 'backup_123',
|
|
354
|
+
region: 'us-east-1',
|
|
355
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await undeploy(createOptions(state));
|
|
360
|
+
|
|
361
|
+
// Verify backup schedule is deleted before postgres
|
|
362
|
+
const backupIndex = calledEndpoints.indexOf('backup.remove:backup_123');
|
|
363
|
+
const postgresIndex = calledEndpoints.indexOf('postgres.remove:pg_123');
|
|
364
|
+
expect(backupIndex).toBeLessThan(postgresIndex);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should delete backup destination', async () => {
|
|
368
|
+
setupMocks();
|
|
369
|
+
|
|
370
|
+
const state = createState({
|
|
371
|
+
backups: {
|
|
372
|
+
bucketName: 'test-bucket',
|
|
373
|
+
bucketArn: 'arn:aws:s3:::test-bucket',
|
|
374
|
+
iamUserName: 'test-user',
|
|
375
|
+
iamAccessKeyId: 'AKIA00000TEST',
|
|
376
|
+
iamSecretAccessKey: 'secret',
|
|
377
|
+
destinationId: 'dest_123',
|
|
378
|
+
region: 'us-east-1',
|
|
379
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const result = await undeploy(createOptions(state));
|
|
384
|
+
|
|
385
|
+
expect(result.deletedBackupDestination).toBe(true);
|
|
386
|
+
expect(calledEndpoints).toContain('destination.remove:dest_123');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should not delete AWS resources by default', async () => {
|
|
390
|
+
setupMocks();
|
|
391
|
+
|
|
392
|
+
const state = createState({
|
|
393
|
+
backups: {
|
|
394
|
+
bucketName: 'test-bucket',
|
|
395
|
+
bucketArn: 'arn:aws:s3:::test-bucket',
|
|
396
|
+
iamUserName: 'test-user',
|
|
397
|
+
iamAccessKeyId: 'AKIA00000TEST',
|
|
398
|
+
iamSecretAccessKey: 'secret',
|
|
399
|
+
destinationId: 'dest_123',
|
|
400
|
+
region: 'us-east-1',
|
|
401
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const result = await undeploy(createOptions(state));
|
|
406
|
+
|
|
407
|
+
expect(result.deletedAwsBackupResources).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('full undeploy', () => {
|
|
412
|
+
it('should delete everything in correct order', async () => {
|
|
413
|
+
setupMocks();
|
|
414
|
+
|
|
415
|
+
const state = createState({
|
|
416
|
+
applications: {
|
|
417
|
+
api: 'app_api',
|
|
418
|
+
web: 'app_web',
|
|
419
|
+
},
|
|
420
|
+
services: {
|
|
421
|
+
postgresId: 'pg_123',
|
|
422
|
+
redisId: 'redis_456',
|
|
423
|
+
},
|
|
424
|
+
backups: {
|
|
425
|
+
bucketName: 'test-bucket',
|
|
426
|
+
bucketArn: 'arn:aws:s3:::test-bucket',
|
|
427
|
+
iamUserName: 'test-user',
|
|
428
|
+
iamAccessKeyId: 'AKIA00000TEST',
|
|
429
|
+
iamSecretAccessKey: 'secret',
|
|
430
|
+
destinationId: 'dest_123',
|
|
431
|
+
postgresBackupId: 'backup_123',
|
|
432
|
+
region: 'us-east-1',
|
|
433
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const result = await undeploy(
|
|
438
|
+
createOptions(state, { deleteProject: true }),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
expect(result.deletedApplications).toEqual(['api', 'web']);
|
|
442
|
+
expect(result.deletedPostgres).toBe(true);
|
|
443
|
+
expect(result.deletedRedis).toBe(true);
|
|
444
|
+
expect(result.deletedBackupDestination).toBe(true);
|
|
445
|
+
expect(result.deletedProject).toBe(true);
|
|
446
|
+
expect(result.errors).toEqual([]);
|
|
447
|
+
|
|
448
|
+
// Verify order: manual backup -> backup schedule -> apps -> postgres -> redis -> destination -> project
|
|
449
|
+
const manualBackupIdx = calledEndpoints.indexOf(
|
|
450
|
+
'backup.manualBackup:backup_123',
|
|
451
|
+
);
|
|
452
|
+
const backupIdx = calledEndpoints.indexOf('backup.remove:backup_123');
|
|
453
|
+
const appIdx = calledEndpoints.indexOf('application.remove:app_api');
|
|
454
|
+
const pgIdx = calledEndpoints.indexOf('postgres.remove:pg_123');
|
|
455
|
+
const redisIdx = calledEndpoints.indexOf('redis.remove:redis_456');
|
|
456
|
+
const destIdx = calledEndpoints.indexOf('destination.remove:dest_123');
|
|
457
|
+
const projIdx = calledEndpoints.indexOf('project.remove:proj_123');
|
|
458
|
+
|
|
459
|
+
expect(manualBackupIdx).toBeLessThan(backupIdx);
|
|
460
|
+
expect(backupIdx).toBeLessThan(appIdx);
|
|
461
|
+
expect(appIdx).toBeLessThan(pgIdx);
|
|
462
|
+
expect(pgIdx).toBeLessThan(redisIdx);
|
|
463
|
+
expect(redisIdx).toBeLessThan(destIdx);
|
|
464
|
+
expect(destIdx).toBeLessThan(projIdx);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should continue on errors and report them', async () => {
|
|
468
|
+
server.use(
|
|
469
|
+
http.post(`${DOKPLOY_BASE_URL}/api/application.remove`, () => {
|
|
470
|
+
calledEndpoints.push('application.remove:failed');
|
|
471
|
+
return HttpResponse.json({ message: 'Error' }, { status: 500 });
|
|
472
|
+
}),
|
|
473
|
+
http.post(`${DOKPLOY_BASE_URL}/api/postgres.remove`, () => {
|
|
474
|
+
calledEndpoints.push('postgres.remove:success');
|
|
475
|
+
return HttpResponse.json({ success: true });
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const state = createState({
|
|
480
|
+
applications: { api: 'app_fail' },
|
|
481
|
+
services: { postgresId: 'pg_123' },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const result = await undeploy(createOptions(state));
|
|
485
|
+
|
|
486
|
+
// Should continue to postgres even though application failed
|
|
487
|
+
expect(calledEndpoints).toContain('application.remove:failed');
|
|
488
|
+
expect(calledEndpoints).toContain('postgres.remove:success');
|
|
489
|
+
expect(result.deletedApplications).toEqual([]);
|
|
490
|
+
expect(result.deletedPostgres).toBe(true);
|
|
491
|
+
expect(result.errors.length).toBe(1);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('deleting DNS records', () => {
|
|
496
|
+
it('should delete DNS records when provider is available', async () => {
|
|
497
|
+
setupMocks();
|
|
498
|
+
const dnsProvider = createMockDnsProvider();
|
|
499
|
+
|
|
500
|
+
const state = createState({
|
|
501
|
+
dnsRecords: {
|
|
502
|
+
'api:A': {
|
|
503
|
+
domain: 'example.com',
|
|
504
|
+
name: 'api',
|
|
505
|
+
type: 'A',
|
|
506
|
+
value: '1.2.3.4',
|
|
507
|
+
ttl: 300,
|
|
508
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
509
|
+
},
|
|
510
|
+
'web:A': {
|
|
511
|
+
domain: 'example.com',
|
|
512
|
+
name: 'web',
|
|
513
|
+
type: 'A',
|
|
514
|
+
value: '1.2.3.4',
|
|
515
|
+
ttl: 300,
|
|
516
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const result = await undeploy(createOptions(state, { dnsProvider }));
|
|
522
|
+
|
|
523
|
+
expect(dnsProvider.deletedRecords).toHaveLength(2);
|
|
524
|
+
expect(result.deletedDnsRecords).toContain('api:A');
|
|
525
|
+
expect(result.deletedDnsRecords).toContain('web:A');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should group DNS records by domain', async () => {
|
|
529
|
+
setupMocks();
|
|
530
|
+
const domainsCalled: string[] = [];
|
|
531
|
+
const dnsProvider: DnsProvider = {
|
|
532
|
+
name: 'mock',
|
|
533
|
+
async getRecords() {
|
|
534
|
+
return [];
|
|
535
|
+
},
|
|
536
|
+
async upsertRecords() {
|
|
537
|
+
return [];
|
|
538
|
+
},
|
|
539
|
+
async deleteRecords(domain, records) {
|
|
540
|
+
domainsCalled.push(domain);
|
|
541
|
+
return records.map((r) => ({
|
|
542
|
+
record: r,
|
|
543
|
+
deleted: true,
|
|
544
|
+
notFound: false,
|
|
545
|
+
}));
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const state = createState({
|
|
550
|
+
dnsRecords: {
|
|
551
|
+
'api:A': {
|
|
552
|
+
domain: 'example.com',
|
|
553
|
+
name: 'api',
|
|
554
|
+
type: 'A',
|
|
555
|
+
value: '1.2.3.4',
|
|
556
|
+
ttl: 300,
|
|
557
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
558
|
+
},
|
|
559
|
+
'api:A:other': {
|
|
560
|
+
domain: 'other.com',
|
|
561
|
+
name: 'api',
|
|
562
|
+
type: 'A',
|
|
563
|
+
value: '5.6.7.8',
|
|
564
|
+
ttl: 300,
|
|
565
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
await undeploy(createOptions(state, { dnsProvider }));
|
|
571
|
+
|
|
572
|
+
expect(domainsCalled).toContain('example.com');
|
|
573
|
+
expect(domainsCalled).toContain('other.com');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should not call DNS provider if no records exist', async () => {
|
|
577
|
+
setupMocks();
|
|
578
|
+
const dnsProvider = createMockDnsProvider();
|
|
579
|
+
|
|
580
|
+
const state = createState({
|
|
581
|
+
dnsRecords: {},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await undeploy(createOptions(state, { dnsProvider }));
|
|
585
|
+
|
|
586
|
+
expect(dnsProvider.deletedRecords).toHaveLength(0);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should handle DNS deletion errors gracefully', async () => {
|
|
590
|
+
setupMocks();
|
|
591
|
+
const dnsProvider = createMockDnsProvider([
|
|
592
|
+
{
|
|
593
|
+
record: { name: 'api', type: 'A' },
|
|
594
|
+
deleted: false,
|
|
595
|
+
notFound: false,
|
|
596
|
+
error: 'Access denied',
|
|
597
|
+
},
|
|
598
|
+
]);
|
|
599
|
+
|
|
600
|
+
const state = createState({
|
|
601
|
+
dnsRecords: {
|
|
602
|
+
'api:A': {
|
|
603
|
+
domain: 'example.com',
|
|
604
|
+
name: 'api',
|
|
605
|
+
type: 'A',
|
|
606
|
+
value: '1.2.3.4',
|
|
607
|
+
ttl: 300,
|
|
608
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const result = await undeploy(createOptions(state, { dnsProvider }));
|
|
614
|
+
|
|
615
|
+
expect(result.deletedDnsRecords).toEqual([]);
|
|
616
|
+
expect(result.errors).toContain(
|
|
617
|
+
'Failed to delete DNS record api:A: Access denied',
|
|
618
|
+
);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('should mark not-found records as deleted', async () => {
|
|
622
|
+
setupMocks();
|
|
623
|
+
const dnsProvider = createMockDnsProvider([
|
|
624
|
+
{
|
|
625
|
+
record: { name: 'api', type: 'A' },
|
|
626
|
+
deleted: false,
|
|
627
|
+
notFound: true,
|
|
628
|
+
},
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
const state = createState({
|
|
632
|
+
dnsRecords: {
|
|
633
|
+
'api:A': {
|
|
634
|
+
domain: 'example.com',
|
|
635
|
+
name: 'api',
|
|
636
|
+
type: 'A',
|
|
637
|
+
value: '1.2.3.4',
|
|
638
|
+
ttl: 300,
|
|
639
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const result = await undeploy(createOptions(state, { dnsProvider }));
|
|
645
|
+
|
|
646
|
+
// Not found means already deleted, so should be in deletedDnsRecords
|
|
647
|
+
expect(result.deletedDnsRecords).toContain('api:A');
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
describe('state updates', () => {
|
|
652
|
+
it('should return updated state with deleted applications removed', async () => {
|
|
653
|
+
setupMocks();
|
|
654
|
+
|
|
655
|
+
const state = createState({
|
|
656
|
+
applications: {
|
|
657
|
+
api: 'app_api',
|
|
658
|
+
web: 'app_web',
|
|
659
|
+
},
|
|
660
|
+
appCredentials: {
|
|
661
|
+
api: { dbUser: 'api_user', dbPassword: 'api_pass' },
|
|
662
|
+
web: { dbUser: 'web_user', dbPassword: 'web_pass' },
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const result = await undeploy(createOptions(state));
|
|
667
|
+
|
|
668
|
+
expect(result.updatedState.applications).toEqual({});
|
|
669
|
+
expect(result.updatedState.appCredentials).toEqual({});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should return updated state with deleted services removed', async () => {
|
|
673
|
+
setupMocks();
|
|
674
|
+
|
|
675
|
+
const state = createState({
|
|
676
|
+
services: {
|
|
677
|
+
postgresId: 'pg_123',
|
|
678
|
+
redisId: 'redis_456',
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const result = await undeploy(createOptions(state));
|
|
683
|
+
|
|
684
|
+
expect(result.updatedState.services.postgresId).toBeUndefined();
|
|
685
|
+
expect(result.updatedState.services.redisId).toBeUndefined();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('should return updated state with deleted DNS records removed', async () => {
|
|
689
|
+
setupMocks();
|
|
690
|
+
const dnsProvider = createMockDnsProvider();
|
|
691
|
+
|
|
692
|
+
const state = createState({
|
|
693
|
+
dnsRecords: {
|
|
694
|
+
'api:A': {
|
|
695
|
+
domain: 'example.com',
|
|
696
|
+
name: 'api',
|
|
697
|
+
type: 'A',
|
|
698
|
+
value: '1.2.3.4',
|
|
699
|
+
ttl: 300,
|
|
700
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
dnsVerified: {
|
|
704
|
+
'api.example.com': {
|
|
705
|
+
serverIp: '1.2.3.4',
|
|
706
|
+
verifiedAt: '2024-01-01T00:00:00.000Z',
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const result = await undeploy(createOptions(state, { dnsProvider }));
|
|
712
|
+
|
|
713
|
+
expect(result.updatedState.dnsRecords).toEqual({});
|
|
714
|
+
expect(result.updatedState.dnsVerified).toEqual({});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should preserve state for failed deletions', async () => {
|
|
718
|
+
server.use(
|
|
719
|
+
http.post(`${DOKPLOY_BASE_URL}/api/application.remove`, () => {
|
|
720
|
+
return HttpResponse.json({ message: 'Error' }, { status: 500 });
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const state = createState({
|
|
725
|
+
applications: { api: 'app_api' },
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const result = await undeploy(createOptions(state));
|
|
729
|
+
|
|
730
|
+
// Application deletion failed, so it should still be in state
|
|
731
|
+
expect(result.updatedState.applications).toEqual({ api: 'app_api' });
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('should clear backups state when deleteBackups is true and succeeds', async () => {
|
|
735
|
+
// This test would need LocalStack for AWS calls
|
|
736
|
+
// For now, just verify the option is passed through
|
|
737
|
+
setupMocks();
|
|
738
|
+
|
|
739
|
+
const state = createState({
|
|
740
|
+
backups: {
|
|
741
|
+
bucketName: 'test-bucket',
|
|
742
|
+
bucketArn: 'arn:aws:s3:::test-bucket',
|
|
743
|
+
iamUserName: 'test-user',
|
|
744
|
+
iamAccessKeyId: 'AKIA00000TEST',
|
|
745
|
+
iamSecretAccessKey: 'secret',
|
|
746
|
+
destinationId: 'dest_123',
|
|
747
|
+
region: 'us-east-1',
|
|
748
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Just verify the destination is deleted (AWS deletion requires LocalStack)
|
|
753
|
+
const result = await undeploy(createOptions(state));
|
|
754
|
+
|
|
755
|
+
expect(result.deletedBackupDestination).toBe(true);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
});
|