@geekmidas/cli 1.5.0 → 1.6.0
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 +17 -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-6JHOwLCx.cjs} +30 -2
- package/dist/{config-ZQM1vBoz.cjs.map → config-6JHOwLCx.cjs.map} +1 -1
- package/dist/{config-DfCJ29PQ.mjs → config-DxASSNjr.mjs} +25 -3
- package/dist/{config-DfCJ29PQ.mjs.map → config-DxASSNjr.mjs.map} +1 -1
- package/dist/config.cjs +3 -2
- package/dist/config.d.cts +14 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +15 -3
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -3
- 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-C0SpUT9Y.d.mts → index-C-KxSGGK.d.mts} +133 -31
- package/dist/index-C-KxSGGK.d.mts.map +1 -0
- package/dist/{index-B58qjyBd.d.cts → index-Cyk2rTyj.d.cts} +132 -30
- package/dist/index-Cyk2rTyj.d.cts.map +1 -0
- package/dist/index.cjs +662 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +626 -116
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BcSjLfWq.mjs → openapi-BYlyAbH3.mjs} +6 -5
- package/dist/openapi-BYlyAbH3.mjs.map +1 -0
- package/dist/{openapi-D6Hcfov0.cjs → openapi-CnvwSRDU.cjs} +6 -5
- package/dist/openapi-CnvwSRDU.cjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +2 -1
- package/dist/openapi.d.mts.map +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-BW2iU37P.mjs → workspace-9IQIjwkQ.mjs} +20 -4
- package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
- package/dist/{workspace-2Do2YcGZ.cjs → workspace-D2ocAlpl.cjs} +20 -4
- package/dist/workspace-D2ocAlpl.cjs.map +1 -0
- package/examples/cron-example.ts +6 -6
- package/examples/function-example.ts +1 -1
- package/package.json +6 -3
- package/src/config.ts +44 -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 +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/dev/__tests__/index.spec.ts +490 -0
- package/src/dev/index.ts +313 -18
- package/src/generators/FunctionGenerator.ts +1 -1
- package/src/generators/Generator.ts +4 -1
- package/src/init/__tests__/generators.spec.ts +167 -18
- package/src/init/__tests__/init.spec.ts +66 -3
- package/src/init/generators/auth.ts +6 -5
- package/src/init/generators/config.ts +49 -7
- package/src/init/generators/docker.ts +8 -8
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -5
- package/src/init/generators/package.ts +4 -0
- package/src/init/generators/test.ts +133 -0
- package/src/init/generators/ui.ts +13 -12
- package/src/init/generators/web.ts +9 -8
- package/src/init/index.ts +2 -0
- package/src/init/templates/api.ts +6 -6
- package/src/init/templates/minimal.ts +2 -2
- package/src/init/templates/worker.ts +2 -2
- package/src/init/versions.ts +3 -3
- package/src/openapi.ts +6 -2
- package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
- package/src/test/__tests__/api.spec.ts +199 -0
- package/src/test/__tests__/auth.spec.ts +162 -0
- package/src/test/__tests__/index.spec.ts +323 -0
- package/src/test/__tests__/web.spec.ts +210 -0
- package/src/test/index.ts +165 -14
- package/src/workspace/__tests__/index.spec.ts +3 -0
- package/src/workspace/index.ts +4 -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
- package/dist/openapi-BcSjLfWq.mjs.map +0 -1
- package/dist/openapi-D6Hcfov0.cjs.map +0 -1
- package/dist/workspace-2Do2YcGZ.cjs.map +0 -1
- package/dist/workspace-BW2iU37P.mjs.map +0 -1
|
@@ -154,6 +154,13 @@ export class DokployApi {
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Delete a project and all its resources
|
|
159
|
+
*/
|
|
160
|
+
async deleteProject(projectId: string): Promise<void> {
|
|
161
|
+
await this.post('project.remove', { projectId });
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
// ============================================
|
|
158
165
|
// Environment endpoints
|
|
159
166
|
// ============================================
|
|
@@ -315,6 +322,13 @@ export class DokployApi {
|
|
|
315
322
|
await this.post('application.deploy', { applicationId });
|
|
316
323
|
}
|
|
317
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Delete an application
|
|
327
|
+
*/
|
|
328
|
+
async deleteApplication(applicationId: string): Promise<void> {
|
|
329
|
+
await this.post('application.remove', { applicationId });
|
|
330
|
+
}
|
|
331
|
+
|
|
318
332
|
// ============================================
|
|
319
333
|
// Registry endpoints
|
|
320
334
|
// ============================================
|
|
@@ -505,6 +519,13 @@ export class DokployApi {
|
|
|
505
519
|
await this.post('postgres.update', { postgresId, ...updates });
|
|
506
520
|
}
|
|
507
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Delete a Postgres database
|
|
524
|
+
*/
|
|
525
|
+
async deletePostgres(postgresId: string): Promise<void> {
|
|
526
|
+
await this.post('postgres.remove', { postgresId });
|
|
527
|
+
}
|
|
528
|
+
|
|
508
529
|
// ============================================
|
|
509
530
|
// Redis endpoints
|
|
510
531
|
// ============================================
|
|
@@ -626,6 +647,13 @@ export class DokployApi {
|
|
|
626
647
|
await this.post('redis.update', { redisId, ...updates });
|
|
627
648
|
}
|
|
628
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Delete a Redis instance
|
|
652
|
+
*/
|
|
653
|
+
async deleteRedis(redisId: string): Promise<void> {
|
|
654
|
+
await this.post('redis.remove', { redisId });
|
|
655
|
+
}
|
|
656
|
+
|
|
629
657
|
// ============================================
|
|
630
658
|
// Domain endpoints
|
|
631
659
|
// ============================================
|
|
@@ -704,6 +732,144 @@ export class DokployApi {
|
|
|
704
732
|
serverId,
|
|
705
733
|
});
|
|
706
734
|
}
|
|
735
|
+
|
|
736
|
+
// ============================================
|
|
737
|
+
// Destination endpoints (backup storage)
|
|
738
|
+
// ============================================
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* List all backup destinations
|
|
742
|
+
*/
|
|
743
|
+
async listDestinations(): Promise<DokployDestination[]> {
|
|
744
|
+
return this.get<DokployDestination[]>('destination.all');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Get a destination by ID
|
|
749
|
+
*/
|
|
750
|
+
async getDestination(destinationId: string): Promise<DokployDestination> {
|
|
751
|
+
return this.get<DokployDestination>(
|
|
752
|
+
`destination.one?destinationId=${destinationId}`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Create a new S3 backup destination
|
|
758
|
+
*/
|
|
759
|
+
async createDestination(
|
|
760
|
+
options: DokployDestinationCreate,
|
|
761
|
+
): Promise<DokployDestination> {
|
|
762
|
+
return this.post<DokployDestination>('destination.create', { ...options });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Find a destination by name
|
|
767
|
+
*/
|
|
768
|
+
async findDestinationByName(
|
|
769
|
+
name: string,
|
|
770
|
+
): Promise<DokployDestination | undefined> {
|
|
771
|
+
const destinations = await this.listDestinations();
|
|
772
|
+
return destinations.find((d) => d.name === name);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Find or create a destination by name
|
|
777
|
+
*/
|
|
778
|
+
async findOrCreateDestination(
|
|
779
|
+
name: string,
|
|
780
|
+
options: Omit<DokployDestinationCreate, 'name'>,
|
|
781
|
+
): Promise<{ destination: DokployDestination; created: boolean }> {
|
|
782
|
+
const existing = await this.findDestinationByName(name);
|
|
783
|
+
if (existing) {
|
|
784
|
+
return { destination: existing, created: false };
|
|
785
|
+
}
|
|
786
|
+
const destination = await this.createDestination({ name, ...options });
|
|
787
|
+
return { destination, created: true };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Update a destination
|
|
792
|
+
*/
|
|
793
|
+
async updateDestination(
|
|
794
|
+
destinationId: string,
|
|
795
|
+
updates: Partial<DokployDestinationCreate>,
|
|
796
|
+
): Promise<void> {
|
|
797
|
+
await this.post('destination.update', { destinationId, ...updates });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Delete a destination
|
|
802
|
+
*/
|
|
803
|
+
async deleteDestination(destinationId: string): Promise<void> {
|
|
804
|
+
await this.post('destination.remove', { destinationId });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Test connection to a destination
|
|
809
|
+
*/
|
|
810
|
+
async testDestinationConnection(
|
|
811
|
+
destinationId: string,
|
|
812
|
+
): Promise<{ success: boolean }> {
|
|
813
|
+
return this.post<{ success: boolean }>('destination.testConnection', {
|
|
814
|
+
destinationId,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ============================================
|
|
819
|
+
// Backup endpoints (scheduled backups)
|
|
820
|
+
// ============================================
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Create a backup schedule for postgres
|
|
824
|
+
*/
|
|
825
|
+
async createPostgresBackup(
|
|
826
|
+
options: DokployBackupCreate,
|
|
827
|
+
): Promise<DokployBackup> {
|
|
828
|
+
return this.post<DokployBackup>('backup.create', {
|
|
829
|
+
...options,
|
|
830
|
+
databaseType: 'postgres',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* List backups for a postgres database
|
|
836
|
+
*/
|
|
837
|
+
async listPostgresBackups(postgresId: string): Promise<DokployBackup[]> {
|
|
838
|
+
return this.get<DokployBackup[]>(
|
|
839
|
+
`backup.all?postgresId=${postgresId}&databaseType=postgres`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Get a backup by ID
|
|
845
|
+
*/
|
|
846
|
+
async getBackup(backupId: string): Promise<DokployBackup> {
|
|
847
|
+
return this.get<DokployBackup>(`backup.one?backupId=${backupId}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Update a backup schedule
|
|
852
|
+
*/
|
|
853
|
+
async updateBackup(
|
|
854
|
+
backupId: string,
|
|
855
|
+
updates: Partial<DokployBackupUpdate>,
|
|
856
|
+
): Promise<void> {
|
|
857
|
+
await this.post('backup.update', { backupId, ...updates });
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Delete a backup schedule
|
|
862
|
+
*/
|
|
863
|
+
async deleteBackup(backupId: string): Promise<void> {
|
|
864
|
+
await this.post('backup.remove', { backupId });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Trigger a manual backup run
|
|
869
|
+
*/
|
|
870
|
+
async runBackupManually(backupId: string): Promise<void> {
|
|
871
|
+
await this.post('backup.manualBackup', { backupId });
|
|
872
|
+
}
|
|
707
873
|
}
|
|
708
874
|
|
|
709
875
|
// ============================================
|
|
@@ -835,6 +1001,77 @@ export interface DokployDomain extends DokployDomainCreate {
|
|
|
835
1001
|
createdAt?: string;
|
|
836
1002
|
}
|
|
837
1003
|
|
|
1004
|
+
// ============================================
|
|
1005
|
+
// Destination types (backup storage)
|
|
1006
|
+
// ============================================
|
|
1007
|
+
|
|
1008
|
+
export interface DokployDestination {
|
|
1009
|
+
destinationId: string;
|
|
1010
|
+
name: string;
|
|
1011
|
+
accessKey: string;
|
|
1012
|
+
bucket: string;
|
|
1013
|
+
region: string;
|
|
1014
|
+
endpoint: string | null;
|
|
1015
|
+
createdAt?: string;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export interface DokployDestinationCreate {
|
|
1019
|
+
/** User-friendly name for the destination */
|
|
1020
|
+
name: string;
|
|
1021
|
+
/** S3 access key ID */
|
|
1022
|
+
accessKey: string;
|
|
1023
|
+
/** S3 secret access key */
|
|
1024
|
+
secretAccessKey: string;
|
|
1025
|
+
/** S3 bucket name */
|
|
1026
|
+
bucket: string;
|
|
1027
|
+
/** AWS region (e.g., 'us-east-1') */
|
|
1028
|
+
region: string;
|
|
1029
|
+
/** Optional endpoint for S3-compatible services */
|
|
1030
|
+
endpoint?: string;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ============================================
|
|
1034
|
+
// Backup types (scheduled backups)
|
|
1035
|
+
// ============================================
|
|
1036
|
+
|
|
1037
|
+
export type DokployDatabaseType = 'postgres' | 'mysql' | 'mariadb' | 'mongo';
|
|
1038
|
+
|
|
1039
|
+
export interface DokployBackup {
|
|
1040
|
+
backupId: string;
|
|
1041
|
+
schedule: string;
|
|
1042
|
+
prefix: string;
|
|
1043
|
+
enabled: boolean;
|
|
1044
|
+
destinationId: string;
|
|
1045
|
+
postgresId?: string;
|
|
1046
|
+
databaseType: DokployDatabaseType;
|
|
1047
|
+
keepLatestCount: number | null;
|
|
1048
|
+
createdAt?: string;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export interface DokployBackupCreate {
|
|
1052
|
+
/** Cron schedule (e.g., '0 2 * * *' for 2 AM daily) */
|
|
1053
|
+
schedule: string;
|
|
1054
|
+
/** Backup file prefix (e.g., 'production/postgres') */
|
|
1055
|
+
prefix: string;
|
|
1056
|
+
/** Destination ID for backup storage */
|
|
1057
|
+
destinationId: string;
|
|
1058
|
+
/** Database name to backup */
|
|
1059
|
+
database: string;
|
|
1060
|
+
/** Postgres instance ID */
|
|
1061
|
+
postgresId: string;
|
|
1062
|
+
/** Enable/disable backup (default: true) */
|
|
1063
|
+
enabled?: boolean;
|
|
1064
|
+
/** Number of backups to keep (default: keep all) */
|
|
1065
|
+
keepLatestCount?: number;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export interface DokployBackupUpdate {
|
|
1069
|
+
schedule?: string;
|
|
1070
|
+
prefix?: string;
|
|
1071
|
+
enabled?: boolean;
|
|
1072
|
+
keepLatestCount?: number;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
838
1075
|
/**
|
|
839
1076
|
* Create a Dokploy API client from stored credentials or environment
|
|
840
1077
|
*/
|
package/src/deploy/index.ts
CHANGED
|
@@ -88,10 +88,13 @@ import {
|
|
|
88
88
|
createEmptyState,
|
|
89
89
|
getAllAppCredentials,
|
|
90
90
|
getApplicationId,
|
|
91
|
+
getBackupState,
|
|
91
92
|
getPostgresId,
|
|
92
93
|
getRedisId,
|
|
93
94
|
setAppCredentials,
|
|
94
95
|
setApplicationId,
|
|
96
|
+
setBackupState,
|
|
97
|
+
setPostgresBackupId,
|
|
95
98
|
setPostgresId,
|
|
96
99
|
setRedisId,
|
|
97
100
|
} from './state.js';
|
|
@@ -325,29 +328,39 @@ async function initializePostgresUsers(
|
|
|
325
328
|
` Creating user "${user.name}" with schema "${schemaName}"...`,
|
|
326
329
|
);
|
|
327
330
|
|
|
328
|
-
// Create or update user
|
|
329
|
-
|
|
330
|
-
DO $$ BEGIN
|
|
331
|
-
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
332
|
-
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
333
|
-
ELSE
|
|
334
|
-
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
335
|
-
END IF;
|
|
336
|
-
END $$;
|
|
337
|
-
`);
|
|
338
|
-
|
|
331
|
+
// Create or update user with all settings in one DO block
|
|
332
|
+
// This avoids "tuple already updated by self" errors from multiple ALTER USER calls
|
|
339
333
|
if (user.usePublicSchema) {
|
|
340
334
|
// API uses public schema
|
|
335
|
+
await client.query(`
|
|
336
|
+
DO $$ BEGIN
|
|
337
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
338
|
+
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
339
|
+
ELSE
|
|
340
|
+
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
341
|
+
END IF;
|
|
342
|
+
END $$;
|
|
343
|
+
`);
|
|
341
344
|
await client.query(`
|
|
342
345
|
GRANT ALL ON SCHEMA public TO "${user.name}";
|
|
343
346
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${user.name}";
|
|
344
347
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${user.name}";
|
|
345
348
|
`);
|
|
346
349
|
} else {
|
|
347
|
-
// Other apps get their own schema
|
|
350
|
+
// Other apps get their own schema - combine user creation and search_path in one block
|
|
351
|
+
await client.query(`
|
|
352
|
+
DO $$ BEGIN
|
|
353
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
354
|
+
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
355
|
+
ELSE
|
|
356
|
+
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
357
|
+
END IF;
|
|
358
|
+
-- Set search_path in same transaction to avoid tuple conflict
|
|
359
|
+
ALTER USER "${user.name}" SET search_path TO "${schemaName}";
|
|
360
|
+
END $$;
|
|
361
|
+
`);
|
|
348
362
|
await client.query(`
|
|
349
363
|
CREATE SCHEMA IF NOT EXISTS "${schemaName}" AUTHORIZATION "${user.name}";
|
|
350
|
-
ALTER USER "${user.name}" SET search_path TO "${schemaName}";
|
|
351
364
|
GRANT USAGE ON SCHEMA "${schemaName}" TO "${user.name}";
|
|
352
365
|
GRANT ALL ON ALL TABLES IN SCHEMA "${schemaName}" TO "${user.name}";
|
|
353
366
|
ALTER DEFAULT PRIVILEGES IN SCHEMA "${schemaName}" GRANT ALL ON TABLES TO "${user.name}";
|
|
@@ -1249,6 +1262,51 @@ export async function workspaceDeployCommand(
|
|
|
1249
1262
|
}
|
|
1250
1263
|
}
|
|
1251
1264
|
|
|
1265
|
+
// ==================================================================
|
|
1266
|
+
// Provision backup destination if configured
|
|
1267
|
+
// ==================================================================
|
|
1268
|
+
if (workspace.deploy?.backups && provisionedPostgres) {
|
|
1269
|
+
logger.log('\n💾 Provisioning backup destination...');
|
|
1270
|
+
|
|
1271
|
+
const { provisionBackupDestination } = await import(
|
|
1272
|
+
'./backup-provisioner.js'
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
const backupState = await provisionBackupDestination({
|
|
1276
|
+
api,
|
|
1277
|
+
projectId: project.projectId,
|
|
1278
|
+
projectName: workspace.name,
|
|
1279
|
+
stage,
|
|
1280
|
+
config: workspace.deploy.backups,
|
|
1281
|
+
existingState: getBackupState(state),
|
|
1282
|
+
logger,
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
// Save backup state
|
|
1286
|
+
setBackupState(state, backupState);
|
|
1287
|
+
|
|
1288
|
+
// Create backup schedule for postgres if not already configured
|
|
1289
|
+
if (!backupState.postgresBackupId) {
|
|
1290
|
+
const backupSchedule = workspace.deploy.backups.schedule ?? '0 2 * * *';
|
|
1291
|
+
const backupRetention = workspace.deploy.backups.retention ?? 30;
|
|
1292
|
+
|
|
1293
|
+
logger.log(' Creating postgres backup schedule...');
|
|
1294
|
+
const backup = await api.createPostgresBackup({
|
|
1295
|
+
schedule: backupSchedule,
|
|
1296
|
+
prefix: `${stage}/postgres`,
|
|
1297
|
+
destinationId: backupState.destinationId,
|
|
1298
|
+
database: provisionedPostgres.databaseName,
|
|
1299
|
+
postgresId: provisionedPostgres.postgresId,
|
|
1300
|
+
enabled: true,
|
|
1301
|
+
keepLatestCount: backupRetention,
|
|
1302
|
+
});
|
|
1303
|
+
setPostgresBackupId(state, backup.backupId);
|
|
1304
|
+
logger.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
|
|
1305
|
+
} else {
|
|
1306
|
+
logger.log(' ✓ Using existing postgres backup schedule');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1252
1310
|
// Track deployed app public URLs for frontend builds
|
|
1253
1311
|
const publicUrls: Record<string, string> = {};
|
|
1254
1312
|
const results: AppDeployResult[] = [];
|
package/src/deploy/state.ts
CHANGED
|
@@ -24,6 +24,48 @@ export interface DnsVerificationRecord {
|
|
|
24
24
|
verifiedAt: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A DNS record that was created during deploy
|
|
29
|
+
*/
|
|
30
|
+
export interface CreatedDnsRecord {
|
|
31
|
+
/** The domain this record belongs to (e.g., 'example.com') */
|
|
32
|
+
domain: string;
|
|
33
|
+
/** Record name/subdomain (e.g., 'api' or '@' for root) */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Record type (A, CNAME, etc.) */
|
|
36
|
+
type: string;
|
|
37
|
+
/** Record value (IP address, hostname, etc.) */
|
|
38
|
+
value: string;
|
|
39
|
+
/** TTL in seconds */
|
|
40
|
+
ttl: number;
|
|
41
|
+
/** When this record was created */
|
|
42
|
+
createdAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Backup destination state
|
|
47
|
+
*/
|
|
48
|
+
export interface BackupState {
|
|
49
|
+
/** S3 bucket name for backups */
|
|
50
|
+
bucketName: string;
|
|
51
|
+
/** S3 bucket ARN */
|
|
52
|
+
bucketArn: string;
|
|
53
|
+
/** IAM user name created for backup access */
|
|
54
|
+
iamUserName: string;
|
|
55
|
+
/** IAM access key ID */
|
|
56
|
+
iamAccessKeyId: string;
|
|
57
|
+
/** IAM secret access key */
|
|
58
|
+
iamSecretAccessKey: string;
|
|
59
|
+
/** Dokploy destination ID */
|
|
60
|
+
destinationId: string;
|
|
61
|
+
/** Dokploy backup schedule ID for postgres (if configured) */
|
|
62
|
+
postgresBackupId?: string;
|
|
63
|
+
/** AWS region where bucket was created */
|
|
64
|
+
region: string;
|
|
65
|
+
/** Timestamp when backup was configured */
|
|
66
|
+
createdAt: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
/**
|
|
28
70
|
* State for a single stage deployment
|
|
29
71
|
*/
|
|
@@ -44,6 +86,10 @@ export interface DokployStageState {
|
|
|
44
86
|
generatedSecrets?: Record<string, Record<string, string>>;
|
|
45
87
|
/** DNS verification state per hostname */
|
|
46
88
|
dnsVerified?: Record<string, DnsVerificationRecord>;
|
|
89
|
+
/** DNS records created during deploy (keyed by "name:type", e.g., "api:A") */
|
|
90
|
+
dnsRecords?: Record<string, CreatedDnsRecord>;
|
|
91
|
+
/** Backup destination state */
|
|
92
|
+
backups?: BackupState;
|
|
47
93
|
lastDeployedAt: string;
|
|
48
94
|
}
|
|
49
95
|
|
|
@@ -309,3 +355,128 @@ export function getAllDnsVerifications(
|
|
|
309
355
|
): Record<string, DnsVerificationRecord> {
|
|
310
356
|
return state?.dnsVerified ?? {};
|
|
311
357
|
}
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// DNS Records
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get the key for a DNS record in state
|
|
365
|
+
*/
|
|
366
|
+
function getDnsRecordKey(name: string, type: string): string {
|
|
367
|
+
return `${name}:${type}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get a created DNS record from state
|
|
372
|
+
*/
|
|
373
|
+
export function getDnsRecord(
|
|
374
|
+
state: DokployStageState | null,
|
|
375
|
+
name: string,
|
|
376
|
+
type: string,
|
|
377
|
+
): CreatedDnsRecord | undefined {
|
|
378
|
+
return state?.dnsRecords?.[getDnsRecordKey(name, type)];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Set a created DNS record in state (mutates state)
|
|
383
|
+
*/
|
|
384
|
+
export function setDnsRecord(
|
|
385
|
+
state: DokployStageState,
|
|
386
|
+
record: Omit<CreatedDnsRecord, 'createdAt'>,
|
|
387
|
+
): void {
|
|
388
|
+
if (!state.dnsRecords) {
|
|
389
|
+
state.dnsRecords = {};
|
|
390
|
+
}
|
|
391
|
+
const key = getDnsRecordKey(record.name, record.type);
|
|
392
|
+
state.dnsRecords[key] = {
|
|
393
|
+
...record,
|
|
394
|
+
createdAt: new Date().toISOString(),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Remove a DNS record from state (mutates state)
|
|
400
|
+
*/
|
|
401
|
+
export function removeDnsRecord(
|
|
402
|
+
state: DokployStageState,
|
|
403
|
+
name: string,
|
|
404
|
+
type: string,
|
|
405
|
+
): void {
|
|
406
|
+
if (state.dnsRecords) {
|
|
407
|
+
delete state.dnsRecords[getDnsRecordKey(name, type)];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get all created DNS records from state
|
|
413
|
+
*/
|
|
414
|
+
export function getAllDnsRecords(
|
|
415
|
+
state: DokployStageState | null,
|
|
416
|
+
): CreatedDnsRecord[] {
|
|
417
|
+
if (!state?.dnsRecords) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
return Object.values(state.dnsRecords);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Clear all DNS records from state (mutates state)
|
|
425
|
+
*/
|
|
426
|
+
export function clearDnsRecords(state: DokployStageState): void {
|
|
427
|
+
state.dnsRecords = {};
|
|
428
|
+
state.dnsVerified = {};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ============================================================================
|
|
432
|
+
// Backup State
|
|
433
|
+
// ============================================================================
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get backup state from state
|
|
437
|
+
*/
|
|
438
|
+
export function getBackupState(
|
|
439
|
+
state: DokployStageState | null,
|
|
440
|
+
): BackupState | undefined {
|
|
441
|
+
return state?.backups;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Set backup state (mutates state)
|
|
446
|
+
*/
|
|
447
|
+
export function setBackupState(
|
|
448
|
+
state: DokployStageState,
|
|
449
|
+
backupState: BackupState,
|
|
450
|
+
): void {
|
|
451
|
+
state.backups = backupState;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get backup destination ID from state
|
|
456
|
+
*/
|
|
457
|
+
export function getBackupDestinationId(
|
|
458
|
+
state: DokployStageState | null,
|
|
459
|
+
): string | undefined {
|
|
460
|
+
return state?.backups?.destinationId;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get postgres backup ID from state
|
|
465
|
+
*/
|
|
466
|
+
export function getPostgresBackupId(
|
|
467
|
+
state: DokployStageState | null,
|
|
468
|
+
): string | undefined {
|
|
469
|
+
return state?.backups?.postgresBackupId;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Set postgres backup ID in state (mutates state)
|
|
474
|
+
*/
|
|
475
|
+
export function setPostgresBackupId(
|
|
476
|
+
state: DokployStageState,
|
|
477
|
+
backupId: string,
|
|
478
|
+
): void {
|
|
479
|
+
if (state.backups) {
|
|
480
|
+
state.backups.postgresBackupId = backupId;
|
|
481
|
+
}
|
|
482
|
+
}
|