@lishugupta652/dokploy 0.1.3
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/.githooks/pre-commit +10 -0
- package/README.md +77 -0
- package/dist/client.js +106 -0
- package/dist/commands/apply.js +38 -0
- package/dist/commands/deploy.js +17 -0
- package/dist/commands/destroy.js +73 -0
- package/dist/commands/init.js +51 -0
- package/dist/commands/projects.js +57 -0
- package/dist/commands/status.js +84 -0
- package/dist/config.js +288 -0
- package/dist/index.js +77 -0
- package/dist/output.js +113 -0
- package/dist/resources/application.js +173 -0
- package/dist/resources/backup.js +248 -0
- package/dist/resources/common.js +89 -0
- package/dist/resources/compose.js +136 -0
- package/dist/resources/database.js +161 -0
- package/dist/resources/domain.js +88 -0
- package/dist/resources/environment.js +88 -0
- package/dist/resources/project.js +52 -0
- package/dist/runtime.js +40 -0
- package/dist/state.js +49 -0
- package/dist/types.js +1 -0
- package/dist/version.js +14 -0
- package/examples/compose-stack.yaml +28 -0
- package/examples/fullstack.yaml +61 -0
- package/examples/simple-app.yaml +39 -0
- package/package.json +51 -0
- package/scripts/bump-version-on-commit.mjs +53 -0
- package/scripts/install-git-hooks.mjs +16 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { z, ZodError } from "zod";
|
|
5
|
+
const nonEmptyString = z.string().trim().min(1);
|
|
6
|
+
const maybeString = z.string().trim().min(1).nullable().optional();
|
|
7
|
+
const envValueSchema = z
|
|
8
|
+
.union([z.string(), z.number(), z.boolean()])
|
|
9
|
+
.transform((value) => String(value));
|
|
10
|
+
const envRecordSchema = z.record(envValueSchema);
|
|
11
|
+
const keyValueSchema = z.union([envRecordSchema, z.string()]);
|
|
12
|
+
const domainSchema = z.object({
|
|
13
|
+
host: nonEmptyString,
|
|
14
|
+
path: z.string().min(1).nullable().optional().default("/"),
|
|
15
|
+
port: z.number().int().min(1).max(65535).nullable().optional(),
|
|
16
|
+
https: z.boolean().optional().default(true),
|
|
17
|
+
certificate: z
|
|
18
|
+
.enum(["letsencrypt", "none", "custom"])
|
|
19
|
+
.optional()
|
|
20
|
+
.default("letsencrypt"),
|
|
21
|
+
customCertResolver: maybeString,
|
|
22
|
+
serviceName: maybeString,
|
|
23
|
+
internalPath: maybeString,
|
|
24
|
+
stripPath: z.boolean().optional().default(false),
|
|
25
|
+
});
|
|
26
|
+
const githubSourceSchema = z.object({
|
|
27
|
+
id: nonEmptyString,
|
|
28
|
+
owner: nonEmptyString,
|
|
29
|
+
repository: nonEmptyString,
|
|
30
|
+
branch: nonEmptyString.optional().default("main"),
|
|
31
|
+
buildPath: nonEmptyString.optional().default("/"),
|
|
32
|
+
triggerType: z.enum(["push", "tag"]).optional().default("push"),
|
|
33
|
+
enableSubmodules: z.boolean().optional().default(false),
|
|
34
|
+
watchPaths: z.array(nonEmptyString).nullable().optional(),
|
|
35
|
+
});
|
|
36
|
+
const gitSourceSchema = z.object({
|
|
37
|
+
url: nonEmptyString,
|
|
38
|
+
branch: nonEmptyString.optional().default("main"),
|
|
39
|
+
buildPath: nonEmptyString.optional().default("/"),
|
|
40
|
+
sshKeyId: maybeString,
|
|
41
|
+
enableSubmodules: z.boolean().optional().default(false),
|
|
42
|
+
watchPaths: z.array(nonEmptyString).nullable().optional(),
|
|
43
|
+
});
|
|
44
|
+
const dockerSourceSchema = z.object({
|
|
45
|
+
image: nonEmptyString,
|
|
46
|
+
username: maybeString,
|
|
47
|
+
password: maybeString,
|
|
48
|
+
registryUrl: maybeString,
|
|
49
|
+
});
|
|
50
|
+
const buildSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
type: z
|
|
53
|
+
.enum([
|
|
54
|
+
"dockerfile",
|
|
55
|
+
"heroku_buildpacks",
|
|
56
|
+
"paketo_buildpacks",
|
|
57
|
+
"nixpacks",
|
|
58
|
+
"static",
|
|
59
|
+
"railpack",
|
|
60
|
+
])
|
|
61
|
+
.optional()
|
|
62
|
+
.default("dockerfile"),
|
|
63
|
+
dockerfile: z.string().nullable().optional(),
|
|
64
|
+
contextPath: z.string().nullable().optional().default("."),
|
|
65
|
+
buildStage: z.string().nullable().optional(),
|
|
66
|
+
herokuVersion: z.string().nullable().optional(),
|
|
67
|
+
railpackVersion: z.string().nullable().optional(),
|
|
68
|
+
publishDirectory: z.string().nullable().optional(),
|
|
69
|
+
isStaticSpa: z.boolean().nullable().optional(),
|
|
70
|
+
})
|
|
71
|
+
.optional()
|
|
72
|
+
.default({});
|
|
73
|
+
const applicationSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
name: nonEmptyString,
|
|
76
|
+
appName: nonEmptyString.optional(),
|
|
77
|
+
description: z.string().nullable().optional(),
|
|
78
|
+
source: z.enum(["github", "git", "docker"]).optional().default("github"),
|
|
79
|
+
github: githubSourceSchema.optional(),
|
|
80
|
+
git: gitSourceSchema.optional(),
|
|
81
|
+
docker: dockerSourceSchema.optional(),
|
|
82
|
+
build: buildSchema,
|
|
83
|
+
env: envRecordSchema.optional().default({}),
|
|
84
|
+
buildArgs: keyValueSchema.nullable().optional(),
|
|
85
|
+
buildSecrets: keyValueSchema.nullable().optional(),
|
|
86
|
+
createEnvFile: z.boolean().optional().default(false),
|
|
87
|
+
domains: z.array(domainSchema).optional().default([]),
|
|
88
|
+
deploy: z.boolean().optional().default(true),
|
|
89
|
+
serverId: maybeString,
|
|
90
|
+
})
|
|
91
|
+
.superRefine((app, ctx) => {
|
|
92
|
+
if (app.source === "github" && !app.github) {
|
|
93
|
+
ctx.addIssue({
|
|
94
|
+
code: z.ZodIssueCode.custom,
|
|
95
|
+
path: ["github"],
|
|
96
|
+
message: "github config is required when source is github",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (app.source === "git" && !app.git) {
|
|
100
|
+
ctx.addIssue({
|
|
101
|
+
code: z.ZodIssueCode.custom,
|
|
102
|
+
path: ["git"],
|
|
103
|
+
message: "git config is required when source is git",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (app.source === "docker" && !app.docker) {
|
|
107
|
+
ctx.addIssue({
|
|
108
|
+
code: z.ZodIssueCode.custom,
|
|
109
|
+
path: ["docker"],
|
|
110
|
+
message: "docker config is required when source is docker",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const composeGitSchema = z.object({
|
|
115
|
+
url: nonEmptyString.optional(),
|
|
116
|
+
id: nonEmptyString.optional(),
|
|
117
|
+
owner: nonEmptyString.optional(),
|
|
118
|
+
repository: nonEmptyString.optional(),
|
|
119
|
+
branch: nonEmptyString.optional().default("main"),
|
|
120
|
+
composePath: nonEmptyString.optional().default("./docker-compose.yml"),
|
|
121
|
+
sshKeyId: maybeString,
|
|
122
|
+
triggerType: z.enum(["push", "tag"]).nullable().optional(),
|
|
123
|
+
enableSubmodules: z.boolean().optional().default(false),
|
|
124
|
+
watchPaths: z.array(nonEmptyString).nullable().optional(),
|
|
125
|
+
});
|
|
126
|
+
const composeSchema = z
|
|
127
|
+
.object({
|
|
128
|
+
name: nonEmptyString,
|
|
129
|
+
appName: nonEmptyString.optional(),
|
|
130
|
+
description: z.string().nullable().optional(),
|
|
131
|
+
source: z
|
|
132
|
+
.enum(["raw", "git", "github", "gitlab", "bitbucket", "gitea"])
|
|
133
|
+
.optional()
|
|
134
|
+
.default("raw"),
|
|
135
|
+
content: z.string().optional(),
|
|
136
|
+
composeFile: z.string().optional(),
|
|
137
|
+
composeType: z.enum(["docker-compose", "stack"]).optional().default("docker-compose"),
|
|
138
|
+
git: composeGitSchema.optional(),
|
|
139
|
+
env: envRecordSchema.optional().default({}),
|
|
140
|
+
domains: z.array(domainSchema).optional().default([]),
|
|
141
|
+
deploy: z.boolean().optional().default(true),
|
|
142
|
+
serverId: maybeString,
|
|
143
|
+
})
|
|
144
|
+
.superRefine((compose, ctx) => {
|
|
145
|
+
if (compose.source === "raw" && !compose.content && !compose.composeFile) {
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: z.ZodIssueCode.custom,
|
|
148
|
+
path: ["content"],
|
|
149
|
+
message: "content or composeFile is required when compose source is raw",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (compose.source !== "raw" && !compose.git) {
|
|
153
|
+
ctx.addIssue({
|
|
154
|
+
code: z.ZodIssueCode.custom,
|
|
155
|
+
path: ["git"],
|
|
156
|
+
message: "git config is required for git-backed compose sources",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const databaseSchema = z.object({
|
|
161
|
+
name: nonEmptyString,
|
|
162
|
+
appName: nonEmptyString.optional(),
|
|
163
|
+
type: z.enum(["postgres", "mysql", "mariadb", "mongo", "redis"]),
|
|
164
|
+
version: nonEmptyString.optional(),
|
|
165
|
+
image: nonEmptyString.optional(),
|
|
166
|
+
description: z.string().nullable().optional(),
|
|
167
|
+
databaseName: nonEmptyString.optional(),
|
|
168
|
+
databaseUser: nonEmptyString.optional(),
|
|
169
|
+
user: nonEmptyString.optional(),
|
|
170
|
+
databasePassword: nonEmptyString.optional(),
|
|
171
|
+
password: nonEmptyString.optional(),
|
|
172
|
+
databaseRootPassword: nonEmptyString.optional(),
|
|
173
|
+
rootPassword: nonEmptyString.optional(),
|
|
174
|
+
replicaSets: z.boolean().nullable().optional(),
|
|
175
|
+
env: envRecordSchema.optional().default({}),
|
|
176
|
+
deploy: z.boolean().optional().default(true),
|
|
177
|
+
serverId: maybeString,
|
|
178
|
+
});
|
|
179
|
+
const backupDestinationSchema = z.object({
|
|
180
|
+
name: nonEmptyString,
|
|
181
|
+
provider: z.string().nullable().optional().default("s3"),
|
|
182
|
+
accessKey: nonEmptyString,
|
|
183
|
+
bucket: nonEmptyString,
|
|
184
|
+
region: nonEmptyString,
|
|
185
|
+
endpoint: nonEmptyString,
|
|
186
|
+
secretAccessKey: nonEmptyString,
|
|
187
|
+
serverId: maybeString,
|
|
188
|
+
});
|
|
189
|
+
const scheduledBackupSchema = z.object({
|
|
190
|
+
name: nonEmptyString,
|
|
191
|
+
destination: nonEmptyString,
|
|
192
|
+
schedule: nonEmptyString,
|
|
193
|
+
enabled: z.boolean().nullable().optional().default(true),
|
|
194
|
+
prefix: nonEmptyString,
|
|
195
|
+
keepLatestCount: z.number().int().positive().nullable().optional(),
|
|
196
|
+
target: nonEmptyString,
|
|
197
|
+
database: nonEmptyString.optional(),
|
|
198
|
+
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
|
199
|
+
backupType: z.enum(["database", "compose"]).optional().default("database"),
|
|
200
|
+
serviceName: z.string().nullable().optional(),
|
|
201
|
+
metadata: z.unknown().nullable().optional(),
|
|
202
|
+
});
|
|
203
|
+
const volumeBackupSchema = z.object({
|
|
204
|
+
name: nonEmptyString,
|
|
205
|
+
destination: nonEmptyString,
|
|
206
|
+
volumeName: nonEmptyString,
|
|
207
|
+
prefix: nonEmptyString,
|
|
208
|
+
cronExpression: nonEmptyString,
|
|
209
|
+
target: nonEmptyString,
|
|
210
|
+
serviceType: z.enum([
|
|
211
|
+
"application",
|
|
212
|
+
"postgres",
|
|
213
|
+
"mysql",
|
|
214
|
+
"mariadb",
|
|
215
|
+
"mongo",
|
|
216
|
+
"redis",
|
|
217
|
+
"compose",
|
|
218
|
+
]),
|
|
219
|
+
appName: nonEmptyString.optional(),
|
|
220
|
+
serviceName: z.string().nullable().optional(),
|
|
221
|
+
turnOff: z.boolean().optional().default(false),
|
|
222
|
+
keepLatestCount: z.number().int().positive().nullable().optional(),
|
|
223
|
+
enabled: z.boolean().nullable().optional().default(true),
|
|
224
|
+
});
|
|
225
|
+
const configSchema = z.object({
|
|
226
|
+
host: z.string().url().optional(),
|
|
227
|
+
stateFile: z.string().optional(),
|
|
228
|
+
project: z.object({
|
|
229
|
+
name: nonEmptyString,
|
|
230
|
+
description: z.string().nullable().optional(),
|
|
231
|
+
env: envRecordSchema.optional().default({}),
|
|
232
|
+
}),
|
|
233
|
+
environment: z
|
|
234
|
+
.object({
|
|
235
|
+
name: nonEmptyString,
|
|
236
|
+
description: z.string().optional().default(""),
|
|
237
|
+
env: envRecordSchema.optional().default({}),
|
|
238
|
+
})
|
|
239
|
+
.optional(),
|
|
240
|
+
applications: z.array(applicationSchema).optional().default([]),
|
|
241
|
+
compose: z.array(composeSchema).optional().default([]),
|
|
242
|
+
databases: z.array(databaseSchema).optional().default([]),
|
|
243
|
+
backupDestinations: z.array(backupDestinationSchema).optional().default([]),
|
|
244
|
+
backups: z.array(scheduledBackupSchema).optional().default([]),
|
|
245
|
+
volumeBackups: z.array(volumeBackupSchema).optional().default([]),
|
|
246
|
+
});
|
|
247
|
+
export async function loadConfig(configPath) {
|
|
248
|
+
const resolvedPath = await resolveConfigPath(configPath);
|
|
249
|
+
const raw = await readFile(resolvedPath, "utf8");
|
|
250
|
+
const parsed = parse(raw);
|
|
251
|
+
try {
|
|
252
|
+
return {
|
|
253
|
+
config: configSchema.parse(parsed),
|
|
254
|
+
configPath: resolvedPath,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error instanceof ZodError) {
|
|
259
|
+
throw new Error(formatZodError(error));
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function resolveConfigPath(configPath) {
|
|
265
|
+
const candidates = configPath ? [configPath] : ["dokploy.yaml", "dokploy.yml"];
|
|
266
|
+
for (const candidate of candidates) {
|
|
267
|
+
const resolved = path.resolve(candidate);
|
|
268
|
+
try {
|
|
269
|
+
await access(resolved);
|
|
270
|
+
return resolved;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
throw new Error(configPath
|
|
277
|
+
? `Config file not found: ${configPath}`
|
|
278
|
+
: "Config file not found. Expected dokploy.yaml or dokploy.yml, or pass -f <path>.");
|
|
279
|
+
}
|
|
280
|
+
function formatZodError(error) {
|
|
281
|
+
const details = error.issues
|
|
282
|
+
.map((issue) => {
|
|
283
|
+
const location = issue.path.length ? issue.path.join(".") : "config";
|
|
284
|
+
return `- ${location}: ${issue.message}`;
|
|
285
|
+
})
|
|
286
|
+
.join("\n");
|
|
287
|
+
return `Invalid Dokploy config:\n${details}`;
|
|
288
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { DokployApiError } from "./client.js";
|
|
4
|
+
import { applyCommand } from "./commands/apply.js";
|
|
5
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
6
|
+
import { destroyCommand } from "./commands/destroy.js";
|
|
7
|
+
import { initCommand } from "./commands/init.js";
|
|
8
|
+
import { projectsCommand } from "./commands/projects.js";
|
|
9
|
+
import { statusCommand } from "./commands/status.js";
|
|
10
|
+
import { readPackageVersion } from "./version.js";
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("dokploy")
|
|
14
|
+
.description("Apply a YAML Dokploy deployment config through the Dokploy API.")
|
|
15
|
+
.version(readPackageVersion());
|
|
16
|
+
program
|
|
17
|
+
.command("projects")
|
|
18
|
+
.description("Show all Dokploy projects from project.all.")
|
|
19
|
+
.option("--host <url>", "Dokploy API host, for example https://dokploy.example.com/api")
|
|
20
|
+
.option("-f, --config <path>", "optional config file path to read host from")
|
|
21
|
+
.option("--summary", "print a concise project list instead of full JSON")
|
|
22
|
+
.option("--json", "print raw JSON without UI formatting")
|
|
23
|
+
.action((options) => run(() => projectsCommand(options)));
|
|
24
|
+
program
|
|
25
|
+
.command("apply")
|
|
26
|
+
.description("Create or update resources from the config.")
|
|
27
|
+
.option("-f, --config <path>", "config file path")
|
|
28
|
+
.option("--state <path>", "state file path")
|
|
29
|
+
.option("--dry-run", "show intended changes without writing resources")
|
|
30
|
+
.action((options) => run(() => applyCommand(options)));
|
|
31
|
+
program
|
|
32
|
+
.command("deploy")
|
|
33
|
+
.description("Redeploy an application or compose stack from state.")
|
|
34
|
+
.argument("<name>", "application or compose name")
|
|
35
|
+
.option("-f, --config <path>", "config file path")
|
|
36
|
+
.option("--state <path>", "state file path")
|
|
37
|
+
.option("--dry-run", "show intended changes without writing resources")
|
|
38
|
+
.action((name, options) => run(() => deployCommand(name, options)));
|
|
39
|
+
program
|
|
40
|
+
.command("status")
|
|
41
|
+
.description("Show remote status for resources tracked in state.")
|
|
42
|
+
.option("-f, --config <path>", "config file path")
|
|
43
|
+
.option("--state <path>", "state file path")
|
|
44
|
+
.action((options) => run(() => statusCommand(options)));
|
|
45
|
+
program
|
|
46
|
+
.command("destroy")
|
|
47
|
+
.description("Delete resources tracked in state. Pass a name to delete one resource.")
|
|
48
|
+
.argument("[name]", "resource name")
|
|
49
|
+
.option("-f, --config <path>", "config file path")
|
|
50
|
+
.option("--state <path>", "state file path")
|
|
51
|
+
.option("--dry-run", "show intended deletes without deleting resources")
|
|
52
|
+
.option("--delete-volumes", "delete compose volumes when deleting compose stacks")
|
|
53
|
+
.action((name, options) => run(() => destroyCommand(name, options)));
|
|
54
|
+
program
|
|
55
|
+
.command("init")
|
|
56
|
+
.description("Generate a starter dokploy.yaml config.")
|
|
57
|
+
.option("-o, --output <path>", "output config path")
|
|
58
|
+
.option("--force", "overwrite an existing file")
|
|
59
|
+
.action((options) => run(() => initCommand(options)));
|
|
60
|
+
await program.parseAsync();
|
|
61
|
+
async function run(action) {
|
|
62
|
+
try {
|
|
63
|
+
await action();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof DokployApiError) {
|
|
67
|
+
console.error(error.message);
|
|
68
|
+
if (process.env.DEBUG) {
|
|
69
|
+
console.error(error.responseBody);
|
|
70
|
+
}
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export class Logger {
|
|
3
|
+
dryRun;
|
|
4
|
+
constructor(dryRun = false) {
|
|
5
|
+
this.dryRun = dryRun;
|
|
6
|
+
}
|
|
7
|
+
info(message) {
|
|
8
|
+
console.log(message);
|
|
9
|
+
}
|
|
10
|
+
muted(message) {
|
|
11
|
+
console.log(chalk.gray(message));
|
|
12
|
+
}
|
|
13
|
+
header(title, subtitle) {
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log(chalk.bold.cyan(title));
|
|
16
|
+
console.log(chalk.gray("=".repeat(Math.max(title.length, 8))));
|
|
17
|
+
if (subtitle) {
|
|
18
|
+
console.log(chalk.gray(subtitle));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
section(title) {
|
|
22
|
+
console.log("");
|
|
23
|
+
console.log(chalk.bold(title));
|
|
24
|
+
console.log(chalk.gray("-".repeat(Math.max(title.length, 8))));
|
|
25
|
+
}
|
|
26
|
+
step(message) {
|
|
27
|
+
console.log(`${chalk.cyan(">")} ${chalk.bold(message)}`);
|
|
28
|
+
}
|
|
29
|
+
success(message) {
|
|
30
|
+
console.log(`${chalk.green("OK")} ${message}`);
|
|
31
|
+
}
|
|
32
|
+
warn(message) {
|
|
33
|
+
console.warn(`${chalk.yellow("WARN")} ${message}`);
|
|
34
|
+
}
|
|
35
|
+
error(message) {
|
|
36
|
+
console.error(`${chalk.red("ERR")} ${message}`);
|
|
37
|
+
}
|
|
38
|
+
action(action, message) {
|
|
39
|
+
const prefix = this.dryRun ? `${chalk.yellow("DRY")} ` : "";
|
|
40
|
+
const color = colorForAction(action);
|
|
41
|
+
console.log(`${prefix}${color(padAction(action))} ${message}`);
|
|
42
|
+
}
|
|
43
|
+
keyValue(label, value) {
|
|
44
|
+
console.log(`${chalk.gray(`${label}:`)} ${formatValue(value)}`);
|
|
45
|
+
}
|
|
46
|
+
json(value) {
|
|
47
|
+
console.log(colorJson(value));
|
|
48
|
+
}
|
|
49
|
+
table(rows, columns) {
|
|
50
|
+
if (rows.length === 0) {
|
|
51
|
+
this.muted("No rows.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const widths = columns.map((column) => {
|
|
55
|
+
const cells = rows.map((row) => String(row[column.key] ?? ""));
|
|
56
|
+
return Math.max(column.label.length, ...cells.map((cell) => stripAnsi(cell).length));
|
|
57
|
+
});
|
|
58
|
+
const header = columns
|
|
59
|
+
.map((column, index) => chalk.bold.cyan(column.label.padEnd(widths[index] ?? 0)))
|
|
60
|
+
.join(chalk.gray(" "));
|
|
61
|
+
console.log(header);
|
|
62
|
+
console.log(chalk.gray(widths.map((width) => "-".repeat(width)).join(" ")));
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const line = columns
|
|
65
|
+
.map((column, index) => String(row[column.key] ?? "").padEnd(widths[index] ?? 0))
|
|
66
|
+
.join(" ");
|
|
67
|
+
console.log(line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
blank() {
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function colorForAction(action) {
|
|
75
|
+
if (action.includes("create"))
|
|
76
|
+
return chalk.green;
|
|
77
|
+
if (action.includes("update") || action === "configured")
|
|
78
|
+
return chalk.blue;
|
|
79
|
+
if (action.includes("delete"))
|
|
80
|
+
return chalk.red;
|
|
81
|
+
if (action.includes("deploy"))
|
|
82
|
+
return chalk.magenta;
|
|
83
|
+
if (action.includes("use") || action === "using")
|
|
84
|
+
return chalk.cyan;
|
|
85
|
+
return chalk.bold;
|
|
86
|
+
}
|
|
87
|
+
function padAction(action) {
|
|
88
|
+
return action.toUpperCase().padEnd(12);
|
|
89
|
+
}
|
|
90
|
+
function formatValue(value) {
|
|
91
|
+
if (value === undefined || value === null || value === "")
|
|
92
|
+
return chalk.gray("-");
|
|
93
|
+
if (typeof value === "number")
|
|
94
|
+
return chalk.yellow(String(value));
|
|
95
|
+
if (typeof value === "boolean")
|
|
96
|
+
return value ? chalk.green("true") : chalk.red("false");
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
function colorJson(value) {
|
|
100
|
+
return JSON.stringify(value, null, 2).replace(/("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, (token) => {
|
|
101
|
+
if (token.startsWith("\"")) {
|
|
102
|
+
return token.endsWith(":") ? chalk.cyan(token) : chalk.green(token);
|
|
103
|
+
}
|
|
104
|
+
if (token === "true" || token === "false")
|
|
105
|
+
return chalk.magenta(token);
|
|
106
|
+
if (token === "null")
|
|
107
|
+
return chalk.gray(token);
|
|
108
|
+
return chalk.yellow(token);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function stripAnsi(value) {
|
|
112
|
+
return value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
113
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { cleanPayload, findByName, formatEnv, getEntityId, getOneById, isDryRunId, slugifyAppName, } from "./common.js";
|
|
2
|
+
import { destroyDomainIds, ensureDomains } from "./domain.js";
|
|
3
|
+
export async function ensureApplication(app, environment, context) {
|
|
4
|
+
const existing = await findApplication(app.name, environment.id, context);
|
|
5
|
+
const ref = existing
|
|
6
|
+
? await updateApplication(app, existing, context)
|
|
7
|
+
: await createApplication(app, environment, context);
|
|
8
|
+
await configureApplication(app, ref, context);
|
|
9
|
+
const domainIds = await ensureDomains(app.domains, { type: "application", id: ref.id }, context);
|
|
10
|
+
if (app.deploy) {
|
|
11
|
+
if (context.dryRun || isDryRunId(ref.id)) {
|
|
12
|
+
context.log.action("would deploy", `application ${app.name}`);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
await context.client.post("application.deploy", {
|
|
16
|
+
applicationId: ref.id,
|
|
17
|
+
title: `Deploy ${app.name}`,
|
|
18
|
+
description: "Triggered by dokploy",
|
|
19
|
+
});
|
|
20
|
+
context.log.action("deployed", `application ${app.name}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const stateRef = {
|
|
24
|
+
...ref,
|
|
25
|
+
domains: domainIds,
|
|
26
|
+
deployed: app.deploy,
|
|
27
|
+
};
|
|
28
|
+
context.state.applications[app.name] = stateRef;
|
|
29
|
+
return stateRef;
|
|
30
|
+
}
|
|
31
|
+
export async function findApplication(name, environmentId, context) {
|
|
32
|
+
const stateRef = context.state.applications[name];
|
|
33
|
+
const fromState = await getOneById(context.client, "application.one", "applicationId", stateRef?.id);
|
|
34
|
+
if (fromState)
|
|
35
|
+
return fromState;
|
|
36
|
+
if (!environmentId || isDryRunId(environmentId))
|
|
37
|
+
return undefined;
|
|
38
|
+
return findByName(context.client, "application.search", name, {
|
|
39
|
+
environmentId,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function createApplication(app, environment, context) {
|
|
43
|
+
if (context.dryRun || isDryRunId(environment.id)) {
|
|
44
|
+
context.log.action("would create", `application ${app.name}`);
|
|
45
|
+
return { id: `dry-run:application:${app.name}`, name: app.name };
|
|
46
|
+
}
|
|
47
|
+
const created = await context.client.post("application.create", cleanPayload({
|
|
48
|
+
name: app.name,
|
|
49
|
+
appName: app.appName ?? slugifyAppName(app.name),
|
|
50
|
+
description: app.description ?? null,
|
|
51
|
+
environmentId: environment.id,
|
|
52
|
+
serverId: app.serverId ?? null,
|
|
53
|
+
}));
|
|
54
|
+
const id = getEntityId(created && typeof created === "object" ? created : undefined, "applicationId") ??
|
|
55
|
+
getEntityId(await findApplication(app.name, environment.id, context), "applicationId");
|
|
56
|
+
if (!id)
|
|
57
|
+
throw new Error(`Created application ${app.name}, but could not resolve its id.`);
|
|
58
|
+
context.log.action("created", `application ${app.name}`);
|
|
59
|
+
return { id, name: app.name };
|
|
60
|
+
}
|
|
61
|
+
async function updateApplication(app, existing, context) {
|
|
62
|
+
const id = getEntityId(existing, "applicationId");
|
|
63
|
+
if (!id)
|
|
64
|
+
throw new Error(`Application ${app.name} exists but no id was returned.`);
|
|
65
|
+
if (context.dryRun) {
|
|
66
|
+
context.log.action("would update", `application ${app.name}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
await context.client.post("application.update", cleanPayload({
|
|
70
|
+
applicationId: id,
|
|
71
|
+
name: app.name,
|
|
72
|
+
appName: app.appName ?? slugifyAppName(app.name),
|
|
73
|
+
description: app.description ?? null,
|
|
74
|
+
}));
|
|
75
|
+
context.log.action("updated", `application ${app.name}`);
|
|
76
|
+
}
|
|
77
|
+
return { id, name: app.name };
|
|
78
|
+
}
|
|
79
|
+
async function configureApplication(app, ref, context) {
|
|
80
|
+
if (context.dryRun || isDryRunId(ref.id)) {
|
|
81
|
+
context.log.action("would configure", `application ${app.name}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (app.source === "github") {
|
|
85
|
+
const github = app.github;
|
|
86
|
+
if (!github)
|
|
87
|
+
throw new Error(`Application ${app.name} is missing github config.`);
|
|
88
|
+
await context.client.post("application.saveGithubProvider", {
|
|
89
|
+
applicationId: ref.id,
|
|
90
|
+
repository: github.repository,
|
|
91
|
+
branch: github.branch,
|
|
92
|
+
owner: github.owner,
|
|
93
|
+
buildPath: github.buildPath,
|
|
94
|
+
githubId: github.id,
|
|
95
|
+
triggerType: github.triggerType,
|
|
96
|
+
enableSubmodules: github.enableSubmodules,
|
|
97
|
+
watchPaths: github.watchPaths ?? null,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (app.source === "git") {
|
|
101
|
+
const git = app.git;
|
|
102
|
+
if (!git)
|
|
103
|
+
throw new Error(`Application ${app.name} is missing git config.`);
|
|
104
|
+
await context.client.post("application.saveGitProvider", {
|
|
105
|
+
applicationId: ref.id,
|
|
106
|
+
customGitUrl: git.url,
|
|
107
|
+
customGitBranch: git.branch,
|
|
108
|
+
customGitBuildPath: git.buildPath,
|
|
109
|
+
customGitSSHKeyId: git.sshKeyId ?? null,
|
|
110
|
+
enableSubmodules: git.enableSubmodules,
|
|
111
|
+
watchPaths: git.watchPaths ?? null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (app.source === "docker") {
|
|
115
|
+
const docker = app.docker;
|
|
116
|
+
if (!docker)
|
|
117
|
+
throw new Error(`Application ${app.name} is missing docker config.`);
|
|
118
|
+
await context.client.post("application.saveDockerProvider", {
|
|
119
|
+
applicationId: ref.id,
|
|
120
|
+
dockerImage: docker.image,
|
|
121
|
+
username: docker.username ?? null,
|
|
122
|
+
password: docker.password ?? null,
|
|
123
|
+
registryUrl: docker.registryUrl ?? null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
await context.client.post("application.saveBuildType", {
|
|
128
|
+
applicationId: ref.id,
|
|
129
|
+
buildType: app.build.type,
|
|
130
|
+
dockerfile: app.build.dockerfile ?? null,
|
|
131
|
+
dockerContextPath: app.build.contextPath ?? null,
|
|
132
|
+
dockerBuildStage: app.build.buildStage ?? null,
|
|
133
|
+
herokuVersion: app.build.herokuVersion ?? null,
|
|
134
|
+
railpackVersion: app.build.railpackVersion ?? null,
|
|
135
|
+
publishDirectory: app.build.publishDirectory ?? null,
|
|
136
|
+
isStaticSpa: app.build.isStaticSpa ?? null,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
await context.client.post("application.saveEnvironment", {
|
|
140
|
+
applicationId: ref.id,
|
|
141
|
+
env: formatEnv(app.env) ?? "",
|
|
142
|
+
buildArgs: formatEnv(app.buildArgs) ?? "",
|
|
143
|
+
buildSecrets: formatEnv(app.buildSecrets) ?? "",
|
|
144
|
+
createEnvFile: app.createEnvFile,
|
|
145
|
+
});
|
|
146
|
+
context.log.action("configured", `application ${app.name}`);
|
|
147
|
+
}
|
|
148
|
+
export async function redeployApplication(ref, context) {
|
|
149
|
+
if (context.dryRun) {
|
|
150
|
+
context.log.action("would redeploy", `application ${ref.name}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
await context.client.post("application.redeploy", {
|
|
154
|
+
applicationId: ref.id,
|
|
155
|
+
title: `Redeploy ${ref.name}`,
|
|
156
|
+
description: "Triggered by dokploy",
|
|
157
|
+
});
|
|
158
|
+
context.log.action("redeployed", `application ${ref.name}`);
|
|
159
|
+
}
|
|
160
|
+
export async function destroyApplication(name, context) {
|
|
161
|
+
const ref = context.state.applications[name];
|
|
162
|
+
if (!ref)
|
|
163
|
+
return;
|
|
164
|
+
await destroyDomainIds(ref.domains, context);
|
|
165
|
+
if (context.dryRun) {
|
|
166
|
+
context.log.action("would delete", `application ${name}`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
await context.client.post("application.delete", { applicationId: ref.id });
|
|
170
|
+
context.log.action("deleted", `application ${name}`);
|
|
171
|
+
}
|
|
172
|
+
delete context.state.applications[name];
|
|
173
|
+
}
|