@mittwald/cli 1.11.2 → 1.13.1-beta.6
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/README.md +5 -0
- package/bin/dev.js +0 -0
- package/dist/commands/app/copy.d.ts +1 -0
- package/dist/commands/app/copy.js +6 -1
- package/dist/commands/app/create/node.d.ts +1 -1
- package/dist/commands/app/create/php-worker.d.ts +1 -1
- package/dist/commands/app/create/php.d.ts +1 -1
- package/dist/commands/app/create/python.d.ts +1 -1
- package/dist/commands/app/create/static.d.ts +1 -1
- package/dist/commands/app/dependency/update.js +4 -6
- package/dist/commands/app/dependency/versions.js +3 -3
- package/dist/commands/app/install/contao.d.ts +1 -1
- package/dist/commands/app/install/joomla.d.ts +1 -1
- package/dist/commands/app/install/matomo.d.ts +1 -1
- package/dist/commands/app/install/nextcloud.d.ts +1 -1
- package/dist/commands/app/install/shopware5.d.ts +1 -1
- package/dist/commands/app/install/shopware6.d.ts +1 -1
- package/dist/commands/app/install/typo3.d.ts +1 -1
- package/dist/commands/app/install/wordpress.d.ts +1 -1
- package/dist/commands/app/open.d.ts +3 -0
- package/dist/commands/app/open.js +35 -9
- package/dist/commands/container/logs.d.ts +1 -0
- package/dist/commands/container/logs.js +8 -0
- package/dist/commands/container/port-forward.js +2 -2
- package/dist/commands/container/run.d.ts +9 -0
- package/dist/commands/container/run.js +42 -4
- package/dist/commands/container/update.js +2 -2
- package/dist/commands/context/get.js +1 -0
- package/dist/commands/database/mysql/create.d.ts +1 -1
- package/dist/commands/database/mysql/create.js +3 -2
- package/dist/commands/ddev/init.js +2 -1
- package/dist/commands/org/invite/list-own.d.ts +1 -0
- package/dist/commands/org/membership/list-own.d.ts +1 -0
- package/dist/commands/project/create.js +12 -3
- package/dist/commands/stack/deploy.d.ts +2 -0
- package/dist/commands/stack/deploy.js +49 -6
- package/dist/lib/context/FlagSetBuilder.d.ts +1 -0
- package/dist/lib/context/FlagSetBuilder.js +7 -1
- package/dist/lib/intellij/config.test.js +9 -2
- package/dist/lib/resources/app/Installer.d.ts +1 -1
- package/dist/lib/resources/app/Installer.js +14 -1
- package/dist/lib/resources/app/flags.d.ts +3 -1
- package/dist/lib/resources/app/flags.js +12 -0
- package/dist/lib/resources/app/install.d.ts +1 -0
- package/dist/lib/resources/app/install.js +1 -0
- package/dist/lib/resources/app/versions.d.ts +7 -1
- package/dist/lib/resources/app/versions.js +35 -2
- package/dist/lib/resources/container/containerconfig.js +7 -1
- package/dist/lib/resources/container/containerconfig.test.d.ts +1 -0
- package/dist/lib/resources/container/containerconfig.test.js +25 -0
- package/dist/lib/resources/database/mysql/flags.js +1 -1
- package/dist/lib/resources/stack/env.d.ts +10 -0
- package/dist/lib/resources/stack/env.js +22 -0
- package/dist/lib/resources/stack/flags.js +16 -1
- package/dist/lib/resources/stack/template-loader.d.ts +18 -0
- package/dist/lib/resources/stack/template-loader.js +94 -0
- package/dist/lib/resources/stack/template-loader.test.d.ts +1 -0
- package/dist/lib/resources/stack/template-loader.test.js +125 -0
- package/dist/lib/units/PortMapping.d.ts +2 -0
- package/dist/lib/units/PortMapping.js +21 -6
- package/dist/lib/units/PortMapping.test.js +10 -0
- package/package.json +13 -9
- package/dist/commands/cronjob/execution/abort.d.ts +0 -18
- package/dist/commands/cronjob/execution/abort.js +0 -41
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Flags } from "@oclif/core";
|
|
3
3
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
4
4
|
import { serverFlags } from "../../lib/resources/server/flags.js";
|
|
@@ -7,6 +7,7 @@ import { Success } from "../../rendering/react/components/Success.js";
|
|
|
7
7
|
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
8
8
|
import { waitFlags, waitUntil } from "../../lib/wait.js";
|
|
9
9
|
import Context from "../../lib/context/Context.js";
|
|
10
|
+
import { Value } from "../../rendering/react/components/Value.js";
|
|
10
11
|
export default class Create extends ExecRenderBaseCommand {
|
|
11
12
|
static description = "Create a new project";
|
|
12
13
|
static flags = {
|
|
@@ -19,7 +20,8 @@ export default class Create extends ExecRenderBaseCommand {
|
|
|
19
20
|
description: "A description for the project.",
|
|
20
21
|
}),
|
|
21
22
|
"update-context": Flags.boolean({
|
|
22
|
-
description: "
|
|
23
|
+
description: "update the CLI context to use the newly created project",
|
|
24
|
+
char: "c",
|
|
23
25
|
}),
|
|
24
26
|
};
|
|
25
27
|
async exec() {
|
|
@@ -48,12 +50,16 @@ export default class Create extends ExecRenderBaseCommand {
|
|
|
48
50
|
}, this.flags["wait-timeout"]);
|
|
49
51
|
stepWaiting.complete();
|
|
50
52
|
}
|
|
53
|
+
const projectResult = await this.apiClient.project.getProject({
|
|
54
|
+
projectId: result.data.id,
|
|
55
|
+
});
|
|
56
|
+
assertStatus(projectResult, 200);
|
|
51
57
|
if (flags["update-context"]) {
|
|
52
58
|
await process.runStep("updating CLI context", async () => {
|
|
53
59
|
await new Context(this.apiClient, this.config).setProjectId(result.data.id);
|
|
54
60
|
});
|
|
55
61
|
}
|
|
56
|
-
process.complete(_jsx(
|
|
62
|
+
await process.complete(_jsx(ProjectCreationSuccess, { shortId: projectResult.data.shortId }));
|
|
57
63
|
return { projectId: result.data.id };
|
|
58
64
|
}
|
|
59
65
|
render({ projectId }) {
|
|
@@ -62,3 +68,6 @@ export default class Create extends ExecRenderBaseCommand {
|
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
}
|
|
71
|
+
function ProjectCreationSuccess({ shortId }) {
|
|
72
|
+
return (_jsxs(Success, { children: ["Your new project ", _jsx(Value, { children: shortId }), " was successfully created! \uD83D\uDE80"] }));
|
|
73
|
+
}
|
|
@@ -8,10 +8,12 @@ export declare class Deploy extends ExecRenderBaseCommand<typeof Deploy, DeployR
|
|
|
8
8
|
static aliases: string[];
|
|
9
9
|
static flags: {
|
|
10
10
|
"compose-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
"from-template": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
"env-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
"stack-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
14
15
|
};
|
|
16
|
+
private loadStackDefinition;
|
|
15
17
|
protected exec(): Promise<DeployResult>;
|
|
16
18
|
protected render({ restartedServices }: DeployResult): ReactNode;
|
|
17
19
|
}
|
|
@@ -3,14 +3,16 @@ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseComm
|
|
|
3
3
|
import { stackFlags, withStackId } from "../../lib/resources/stack/flags.js";
|
|
4
4
|
import { Flags } from "@oclif/core";
|
|
5
5
|
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
6
|
-
import { loadStackFromFile } from "../../lib/resources/stack/loader.js";
|
|
6
|
+
import { loadStackFromFile, loadStackFromStr, } from "../../lib/resources/stack/loader.js";
|
|
7
7
|
import { assertStatus } from "@mittwald/api-client";
|
|
8
8
|
import assertSuccess from "../../lib/apiutil/assert_success.js";
|
|
9
|
-
import { collectEnvironment } from "../../lib/resources/stack/env.js";
|
|
9
|
+
import { collectEnvironment, fillMissingEnvironmentVariables, } from "../../lib/resources/stack/env.js";
|
|
10
10
|
import { sanitizeStackDefinition } from "../../lib/resources/stack/sanitize.js";
|
|
11
11
|
import { enrichStackDefinition } from "../../lib/resources/stack/enrich.js";
|
|
12
12
|
import { Success } from "../../rendering/react/components/Success.js";
|
|
13
13
|
import { Value } from "../../rendering/react/components/Value.js";
|
|
14
|
+
import { loadStackFromTemplate } from "../../lib/resources/stack/template-loader.js";
|
|
15
|
+
import { parse } from "envfile";
|
|
14
16
|
export class Deploy extends ExecRenderBaseCommand {
|
|
15
17
|
static description = "Deploys a docker-compose compatible file to a mittwald container stack";
|
|
16
18
|
static aliases = ["stack:up"];
|
|
@@ -21,22 +23,63 @@ export class Deploy extends ExecRenderBaseCommand {
|
|
|
21
23
|
summary: 'path to a compose file, or "-" to read from stdin',
|
|
22
24
|
default: "./docker-compose.yml",
|
|
23
25
|
char: "c",
|
|
26
|
+
exclusive: ["from-template"],
|
|
27
|
+
}),
|
|
28
|
+
"from-template": Flags.string({
|
|
29
|
+
summary: "deploy from a GitHub template (e.g., mittwald/n8n)",
|
|
30
|
+
description: `\
|
|
31
|
+
Fetch and deploy a stack from a GitHub template repository. Template names are automatically converted to repository names by prefixing "stack-template-" to the name part.
|
|
32
|
+
|
|
33
|
+
For example, "mittwald/n8n" resolves to the repository "mittwald/stack-template-n8n". The command fetches both docker-compose.yml and .env files from the main branch.
|
|
34
|
+
|
|
35
|
+
Environment variable precedence (from lowest to highest):
|
|
36
|
+
1. Template .env file (if present in the repository)
|
|
37
|
+
2. System environment variables (process.env)
|
|
38
|
+
3. Local --env-file (if specified)
|
|
39
|
+
|
|
40
|
+
This flag is mutually exclusive with --compose-file.`,
|
|
41
|
+
exclusive: ["compose-file"],
|
|
24
42
|
}),
|
|
25
43
|
"env-file": Flags.file({
|
|
26
44
|
summary: "alternative path to file with environment variables",
|
|
27
45
|
default: "./.env",
|
|
28
46
|
}),
|
|
29
47
|
};
|
|
48
|
+
async loadStackDefinition(source, envFile, existing, renderer) {
|
|
49
|
+
// Build environment: start with process.env, then template .env, then local --env-file
|
|
50
|
+
let env = { ...process.env };
|
|
51
|
+
if ("template" in source) {
|
|
52
|
+
const hasServices = existing.services?.length ?? 0 > 0;
|
|
53
|
+
if (hasServices) {
|
|
54
|
+
throw new Error("Re-applying templates to existing stacks is currently not supported.");
|
|
55
|
+
}
|
|
56
|
+
// Load from GitHub template
|
|
57
|
+
const { composeYaml, envContent } = await renderer.runStep("fetching template from GitHub", () => loadStackFromTemplate(source.template));
|
|
58
|
+
if (envContent) {
|
|
59
|
+
const templateEnv = parse(envContent);
|
|
60
|
+
env = { ...env, ...templateEnv };
|
|
61
|
+
}
|
|
62
|
+
env = await collectEnvironment(env, envFile);
|
|
63
|
+
env = await fillMissingEnvironmentVariables(env, renderer);
|
|
64
|
+
return loadStackFromStr(composeYaml, env);
|
|
65
|
+
}
|
|
66
|
+
// Load from local file
|
|
67
|
+
env = await collectEnvironment(env, envFile);
|
|
68
|
+
return loadStackFromFile(source.composeFile, env);
|
|
69
|
+
}
|
|
30
70
|
async exec() {
|
|
31
71
|
const stackId = await withStackId(this.apiClient, Deploy, this.flags, this.args, this.config);
|
|
32
|
-
const { "compose-file": composeFile, "env-file": envFile } = this.flags;
|
|
72
|
+
const { "compose-file": composeFile, "from-template": fromTemplate, "env-file": envFile, } = this.flags;
|
|
33
73
|
const r = makeProcessRenderer(this.flags, "Deploying container stack");
|
|
74
|
+
const existingStack = await r.runStep("retrieving current stack state", async () => {
|
|
75
|
+
const resp = await this.apiClient.container.getStack({ stackId });
|
|
76
|
+
assertStatus(resp, 200);
|
|
77
|
+
return resp.data;
|
|
78
|
+
});
|
|
34
79
|
const result = { restartedServices: [] };
|
|
35
|
-
|
|
36
|
-
let stackDefinition = await loadStackFromFile(composeFile, env);
|
|
80
|
+
let stackDefinition = await this.loadStackDefinition(fromTemplate ? { template: fromTemplate } : { composeFile }, envFile, existingStack, r);
|
|
37
81
|
stackDefinition = sanitizeStackDefinition(stackDefinition);
|
|
38
82
|
stackDefinition = await r.runStep("getting image configurations", () => enrichStackDefinition(stackDefinition));
|
|
39
|
-
this.debug("complete stack definition: %O", stackDefinition);
|
|
40
83
|
const declaredStack = await r.runStep("deploying stack", async () => {
|
|
41
84
|
const resp = await this.apiClient.container.declareStack({
|
|
42
85
|
stackId,
|
|
@@ -27,6 +27,7 @@ export type FlagSetOptions = {
|
|
|
27
27
|
normalize: NormalizeFn;
|
|
28
28
|
displayName: string;
|
|
29
29
|
retrieveFromContext: boolean;
|
|
30
|
+
retrieveFunction: (client: MittwaldAPIV2Client, ctx: Context) => Promise<string | null>;
|
|
30
31
|
expectedShortIDFormat: {
|
|
31
32
|
pattern: RegExp;
|
|
32
33
|
display: string;
|
|
@@ -119,7 +119,7 @@ export default class FlagSetBuilder {
|
|
|
119
119
|
buildIDGetter() {
|
|
120
120
|
const idInputSanityCheck = this.buildSanityCheck();
|
|
121
121
|
const idFromArgsOrFlag = this.buildIDFromArgsOrFlag();
|
|
122
|
-
const { normalize = (_, id) => id, retrieveFromContext = true } = this.opts;
|
|
122
|
+
const { normalize = (_, id) => id, retrieveFromContext = true, retrieveFunction, } = this.opts;
|
|
123
123
|
return async (apiClient, commandType, flags, args, cfg) => {
|
|
124
124
|
const context = new Context(apiClient, cfg);
|
|
125
125
|
const idInput = idFromArgsOrFlag(flags, args);
|
|
@@ -133,6 +133,12 @@ export default class FlagSetBuilder {
|
|
|
133
133
|
return idFromContext.value;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
if (retrieveFunction) {
|
|
137
|
+
const idFromFunction = await retrieveFunction(apiClient, context);
|
|
138
|
+
if (idFromFunction) {
|
|
139
|
+
return idFromFunction;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
136
142
|
throw makeMissingContextInputError(commandType, this.name, this.flagName);
|
|
137
143
|
};
|
|
138
144
|
}
|
|
@@ -7,7 +7,10 @@ import { generateIntellijConfigs } from "./config.js";
|
|
|
7
7
|
describe("IntelliJ Config Generator", () => {
|
|
8
8
|
let tempDir;
|
|
9
9
|
let testData;
|
|
10
|
-
const parser = new XMLParser({
|
|
10
|
+
const parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
});
|
|
11
14
|
beforeEach(() => {
|
|
12
15
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "intellij-test-"));
|
|
13
16
|
testData = {
|
|
@@ -74,7 +77,11 @@ describe("IntelliJ Config Generator", () => {
|
|
|
74
77
|
});
|
|
75
78
|
test("should add multiple SSH configs for different hosts", () => {
|
|
76
79
|
generateIntellijConfigs(testData, tempDir);
|
|
77
|
-
const differentHostData = {
|
|
80
|
+
const differentHostData = {
|
|
81
|
+
...testData,
|
|
82
|
+
host: "ssh.other.example.com",
|
|
83
|
+
appShortId: "app456",
|
|
84
|
+
};
|
|
78
85
|
generateIntellijConfigs(differentHostData, tempDir);
|
|
79
86
|
const configPath = path.join(tempDir, ".idea", "sshConfigs.xml");
|
|
80
87
|
const content = fs.readFileSync(configPath, "utf8");
|
|
@@ -5,7 +5,7 @@ import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
|
5
5
|
import { Config } from "@oclif/core";
|
|
6
6
|
type AppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
|
|
7
7
|
type AppInstallation = MittwaldAPIV2.Components.Schemas.AppAppInstallation;
|
|
8
|
-
type ImplicitDefaultFlag = "wait" | "wait-timeout" | "site-title";
|
|
8
|
+
type ImplicitDefaultFlag = "wait" | "wait-timeout" | "site-title" | "install-path" | "update-context";
|
|
9
9
|
export interface AppInstallationResult {
|
|
10
10
|
appInstallation: AppInstallation;
|
|
11
11
|
appVersion: AppVersion;
|
|
@@ -7,6 +7,7 @@ import { triggerAppInstallation } from "./install.js";
|
|
|
7
7
|
import { waitUntilAppStateHasNormalized } from "./wait.js";
|
|
8
8
|
import { Success } from "../../../rendering/react/components/Success.js";
|
|
9
9
|
import AppUsageHints from "../../../rendering/react/components/AppInstallation/AppUsageHints.js";
|
|
10
|
+
import Context from "../../context/Context.js";
|
|
10
11
|
export class AppInstaller {
|
|
11
12
|
appId;
|
|
12
13
|
appName;
|
|
@@ -25,7 +26,14 @@ export class AppInstaller {
|
|
|
25
26
|
this.description = AppInstaller.makeDescription(appName);
|
|
26
27
|
}
|
|
27
28
|
get flags() {
|
|
28
|
-
const flags = provideSupportedFlags([
|
|
29
|
+
const flags = provideSupportedFlags([
|
|
30
|
+
...this.appSupportedFlags,
|
|
31
|
+
"wait",
|
|
32
|
+
"wait-timeout",
|
|
33
|
+
"site-title",
|
|
34
|
+
"install-path",
|
|
35
|
+
"update-context",
|
|
36
|
+
], this.appName);
|
|
29
37
|
if (this.mutateFlags) {
|
|
30
38
|
this.mutateFlags(flags);
|
|
31
39
|
}
|
|
@@ -45,6 +53,11 @@ export class AppInstaller {
|
|
|
45
53
|
else {
|
|
46
54
|
successText = `Your ${this.appName} installation has started. Have fun when it's ready! 🎉`;
|
|
47
55
|
}
|
|
56
|
+
if (flags["update-context"]) {
|
|
57
|
+
const context = new Context(apiClient, config);
|
|
58
|
+
await context.setProjectId(appInstallation.projectId);
|
|
59
|
+
await context.setAppInstallationId(appInstallation.id);
|
|
60
|
+
}
|
|
48
61
|
await process.complete(_jsx(Success, { children: successText }));
|
|
49
62
|
return {
|
|
50
63
|
appInstallation,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
import { ProcessRenderer } from "../../../rendering/process/process.js";
|
|
3
3
|
import { ProcessFlags } from "../../../rendering/process/process_flags.js";
|
|
4
|
-
import { FlagInput, OptionFlag, OutputFlags } from "@oclif/core/interfaces";
|
|
4
|
+
import { BooleanFlag, FlagInput, OptionFlag, OutputFlags } from "@oclif/core/interfaces";
|
|
5
5
|
import { waitFlags } from "../../wait.js";
|
|
6
6
|
export declare const appInstallationFlags: import("../../context/FlagSetBuilder.js").ContextFlags<"installation">, appInstallationArgs: import("../../context/FlagSetBuilder.js").ContextArgs<"installation">, withAppInstallationId: (apiClient: MittwaldAPIV2Client, command: "flag" | "arg" | import("../../context/FlagSetBuilder.js").CommandType<"installation">, flags: {
|
|
7
7
|
[k: string]: unknown;
|
|
@@ -22,10 +22,12 @@ type AvailableFlags = typeof waitFlags & {
|
|
|
22
22
|
"shop-lang": OptionFlag<string | undefined>;
|
|
23
23
|
"shop-currency": OptionFlag<string | undefined>;
|
|
24
24
|
"install-mode": OptionFlag<string>;
|
|
25
|
+
"install-path": OptionFlag<string>;
|
|
25
26
|
"document-root": OptionFlag<string>;
|
|
26
27
|
"opensearch-host": OptionFlag<string>;
|
|
27
28
|
"opensearch-port": OptionFlag<string>;
|
|
28
29
|
entrypoint: OptionFlag<string | undefined>;
|
|
30
|
+
"update-context": BooleanFlag<boolean>;
|
|
29
31
|
};
|
|
30
32
|
export type RelevantFlags<TFlags extends readonly AvailableFlagName[]> = ProcessFlags & Pick<AvailableFlags, TFlags[number]>;
|
|
31
33
|
export type RelevantFlagInput<TFlags extends readonly AvailableFlagName[]> = FlagInput<RelevantFlags<TFlags>>;
|
|
@@ -113,6 +113,12 @@ function buildFlagsWithDescription(appName) {
|
|
|
113
113
|
options: ["composer", "symlink"],
|
|
114
114
|
default: "composer",
|
|
115
115
|
}),
|
|
116
|
+
"install-path": Flags.string({
|
|
117
|
+
required: false,
|
|
118
|
+
summary: `the installation path of your ${appName} application`,
|
|
119
|
+
description: "This is the path where your application will be installed. If omitted, this will default to an automatically-generated path.",
|
|
120
|
+
default: undefined,
|
|
121
|
+
}),
|
|
116
122
|
"document-root": Flags.string({
|
|
117
123
|
required: true,
|
|
118
124
|
summary: `the document root from which your ${appName} will be served (relative to the installation path)`,
|
|
@@ -137,6 +143,12 @@ function buildFlagsWithDescription(appName) {
|
|
|
137
143
|
required: false,
|
|
138
144
|
default: undefined,
|
|
139
145
|
}),
|
|
146
|
+
"update-context": Flags.boolean({
|
|
147
|
+
description: "update the CLI context to use the newly created app installation",
|
|
148
|
+
char: "c",
|
|
149
|
+
required: false,
|
|
150
|
+
default: false,
|
|
151
|
+
}),
|
|
140
152
|
...waitFlags,
|
|
141
153
|
};
|
|
142
154
|
}
|
|
@@ -5,6 +5,7 @@ type AppAppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
|
|
|
5
5
|
export declare function triggerAppInstallation(apiClient: MittwaldAPIV2Client, process: ProcessRenderer, projectId: string, flags: {
|
|
6
6
|
"site-title": string;
|
|
7
7
|
"document-root"?: string;
|
|
8
|
+
"install-path"?: string;
|
|
8
9
|
} & {
|
|
9
10
|
[k: string]: unknown;
|
|
10
11
|
}, appVersion: AppAppVersion): Promise<AppAppInstallation>;
|
|
@@ -7,6 +7,7 @@ export async function triggerAppInstallation(apiClient, process, projectId, flag
|
|
|
7
7
|
appVersionId: appVersion.id,
|
|
8
8
|
description: flags["site-title"],
|
|
9
9
|
updatePolicy: "none",
|
|
10
|
+
installationPath: flags["install-path"],
|
|
10
11
|
userInputs: Object.keys(flags).map((k) => ({
|
|
11
12
|
name: k.replace("-", "_"),
|
|
12
13
|
value: flags[k],
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
import { ProcessRenderer } from "../../../rendering/process/process.js";
|
|
3
3
|
type AppVersion = MittwaldAPIV2.Components.Schemas.AppAppVersion;
|
|
4
|
+
type ObjectWithVersions = {
|
|
5
|
+
internalVersion: string;
|
|
6
|
+
externalVersion: string;
|
|
7
|
+
};
|
|
4
8
|
export declare function normalizeToAppVersionUuid(apiClient: MittwaldAPIV2Client, version: string, process: ProcessRenderer, appUuid: string): Promise<MittwaldAPIV2.Components.Schemas.AppAppVersion>;
|
|
5
9
|
export declare function getLatestAvailableAppVersionForApp(apiClient: MittwaldAPIV2Client, appId: string): Promise<AppVersion | undefined>;
|
|
6
10
|
export declare function getAllUpgradeCandidatesFromAppInstallationId(apiClient: MittwaldAPIV2Client, appInstallationId: string): Promise<AppVersion[]>;
|
|
7
11
|
export declare function getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates(apiClient: MittwaldAPIV2Client, appId: string, baseAppVersionId: string): Promise<AppVersion | undefined>;
|
|
8
12
|
export declare function getAvailableTargetAppVersionFromExternalVersion(apiClient: MittwaldAPIV2Client, appId: string, baseAppVersionId: string, targetExternalVersion: string): Promise<AppVersion | undefined>;
|
|
9
13
|
export declare function getAppVersionUuidFromAppVersion(apiClient: MittwaldAPIV2Client, appId: string, appVersion: string | undefined): Promise<AppVersion | undefined>;
|
|
10
|
-
export declare function sortArrayByExternalVersion(versions:
|
|
14
|
+
export declare function sortArrayByExternalVersion<T extends ObjectWithVersions>(versions: T[]): T[];
|
|
15
|
+
export declare function sortArrayByInternalVersion<T extends ObjectWithVersions>(versions: T[]): T[];
|
|
16
|
+
export declare function compareVersionsBy<T extends ObjectWithVersions>(field: "internal" | "external"): (a: T, b: T) => -1 | 0 | 1;
|
|
11
17
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
2
|
-
import { gt } from "semver";
|
|
2
|
+
import { coerce, gt } from "semver";
|
|
3
3
|
import { getAppInstallationFromUuid, getAppNameFromUuid } from "./uuid.js";
|
|
4
4
|
import { compare } from "semver";
|
|
5
5
|
export async function normalizeToAppVersionUuid(apiClient, version, process, appUuid) {
|
|
@@ -78,5 +78,38 @@ export async function getAppVersionUuidFromAppVersion(apiClient, appId, appVersi
|
|
|
78
78
|
item.externalVersion === appVersion);
|
|
79
79
|
}
|
|
80
80
|
export function sortArrayByExternalVersion(versions) {
|
|
81
|
-
return versions.sort((
|
|
81
|
+
return versions.sort(compareVersionsBy("external"));
|
|
82
|
+
}
|
|
83
|
+
export function sortArrayByInternalVersion(versions) {
|
|
84
|
+
return versions.sort(compareVersionsBy("internal"));
|
|
85
|
+
}
|
|
86
|
+
export function compareVersionsBy(field) {
|
|
87
|
+
const fullField = `${field}Version`;
|
|
88
|
+
return (a, b) => {
|
|
89
|
+
const aCoerced = coerce(a[fullField]);
|
|
90
|
+
const bCoerced = coerce(b[fullField]);
|
|
91
|
+
if (!aCoerced || !bCoerced) {
|
|
92
|
+
return naiveVersionCompare(a.internalVersion, b.internalVersion);
|
|
93
|
+
}
|
|
94
|
+
return compare(aCoerced, bCoerced);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A naive version comparison function that compares version strings in the
|
|
99
|
+
* format "x.y.z". This function does not handle pre-release or build metadata.
|
|
100
|
+
*/
|
|
101
|
+
function naiveVersionCompare(a, b) {
|
|
102
|
+
const aParts = a.split(".").map((part) => parseInt(part, 10));
|
|
103
|
+
const bParts = b.split(".").map((part) => parseInt(part, 10));
|
|
104
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
105
|
+
const aPart = aParts[i] || 0;
|
|
106
|
+
const bPart = bParts[i] || 0;
|
|
107
|
+
if (aPart > bPart) {
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
if (aPart < bPart) {
|
|
111
|
+
return -1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
82
115
|
}
|
|
@@ -40,7 +40,13 @@ export async function parseEnvironmentVariablesFromFile(envFiles = []) {
|
|
|
40
40
|
* @returns An object containing environment variable key-value pairs
|
|
41
41
|
*/
|
|
42
42
|
export function parseEnvironmentVariablesFromEnvFlags(envFlags = []) {
|
|
43
|
-
const splitIntoKeyAndValue = (e) =>
|
|
43
|
+
const splitIntoKeyAndValue = (e) => {
|
|
44
|
+
const index = e.indexOf("=");
|
|
45
|
+
if (index < 0) {
|
|
46
|
+
throw new Error(`Invalid environment variable format: ${e}`);
|
|
47
|
+
}
|
|
48
|
+
return [e.slice(0, index), e.slice(index + 1)];
|
|
49
|
+
};
|
|
44
50
|
return Object.fromEntries(envFlags.map(splitIntoKeyAndValue));
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { parseEnvironmentVariablesFromEnvFlags } from "./containerconfig.js";
|
|
3
|
+
describe("Containerconfig handling", () => {
|
|
4
|
+
describe("Config parsing", () => {
|
|
5
|
+
test("call parser with simple flags", () => {
|
|
6
|
+
const args = ["foo=bar", "ham=eggs"];
|
|
7
|
+
const res = parseEnvironmentVariablesFromEnvFlags(args);
|
|
8
|
+
expect(res).toStrictEqual({
|
|
9
|
+
foo: "bar",
|
|
10
|
+
ham: "eggs",
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
test("call parser with flag containing another '='", () => {
|
|
14
|
+
const args = ["extra_args=first=1 second=2 third=littlebitoflove"];
|
|
15
|
+
const res = parseEnvironmentVariablesFromEnvFlags(args);
|
|
16
|
+
expect(res).toStrictEqual({
|
|
17
|
+
extra_args: "first=1 second=2 third=littlebitoflove",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
test("throw error for invalid flag format", () => {
|
|
21
|
+
const args = ["invalidFlagWithoutEqualsSign"];
|
|
22
|
+
expect(() => parseEnvironmentVariablesFromEnvFlags(args)).toThrow("Invalid environment variable format: invalidFlagWithoutEqualsSign");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -3,7 +3,7 @@ import { assertStatus } from "@mittwald/api-client-commons";
|
|
|
3
3
|
export const mysqlConnectionFlags = {
|
|
4
4
|
"mysql-password": Flags.string({
|
|
5
5
|
char: "p",
|
|
6
|
-
summary: "the password to use for the MySQL user
|
|
6
|
+
summary: "the password to use for the MySQL user",
|
|
7
7
|
description: `\
|
|
8
8
|
The password to use for the MySQL user. If not provided, the environment variable MYSQL_PWD will be used. If that is not set either, the command will interactively ask for the password.
|
|
9
9
|
|
|
@@ -1 +1,11 @@
|
|
|
1
|
+
import { ProcessRenderer } from "../../../rendering/process/process.js";
|
|
1
2
|
export declare function collectEnvironment(base: NodeJS.ProcessEnv, envFile: string): Promise<Record<string, string | undefined>>;
|
|
3
|
+
/**
|
|
4
|
+
* Fills in missing environment variables in the base environment.
|
|
5
|
+
*
|
|
6
|
+
* This is done by checking for special placeholder values:
|
|
7
|
+
*
|
|
8
|
+
* - `__PROMPT__`: prompts the user to input a value.
|
|
9
|
+
* - `__GENERATE__`: generates a random base64-encoded string.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fillMissingEnvironmentVariables(base: NodeJS.ProcessEnv, renderer: ProcessRenderer): Promise<Record<string, string | undefined>>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import { parse } from "envfile";
|
|
3
3
|
import { pathExists } from "../../util/fs/pathExists.js";
|
|
4
|
+
import { getRandomValues } from "node:crypto";
|
|
4
5
|
export async function collectEnvironment(base, envFile) {
|
|
5
6
|
if (!(await pathExists(envFile))) {
|
|
6
7
|
return base;
|
|
@@ -9,3 +10,24 @@ export async function collectEnvironment(base, envFile) {
|
|
|
9
10
|
const parsed = parse(defs);
|
|
10
11
|
return { ...base, ...parsed };
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Fills in missing environment variables in the base environment.
|
|
15
|
+
*
|
|
16
|
+
* This is done by checking for special placeholder values:
|
|
17
|
+
*
|
|
18
|
+
* - `__PROMPT__`: prompts the user to input a value.
|
|
19
|
+
* - `__GENERATE__`: generates a random base64-encoded string.
|
|
20
|
+
*/
|
|
21
|
+
export async function fillMissingEnvironmentVariables(base, renderer) {
|
|
22
|
+
const output = { ...base };
|
|
23
|
+
for (const [key, value] of Object.entries(base)) {
|
|
24
|
+
if (value === "__PROMPT__") {
|
|
25
|
+
output[key] = await renderer.addInput(`enter value for environment variable ${key}`, false);
|
|
26
|
+
}
|
|
27
|
+
if (value === "__GENERATE__") {
|
|
28
|
+
renderer.addInfo(`generating random value for environment variable ${key}`);
|
|
29
|
+
output[key] = Buffer.from(getRandomValues(new Uint8Array(32))).toString("base64");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
import FlagSetBuilder from "../../context/FlagSetBuilder.js";
|
|
2
|
-
|
|
2
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
3
|
+
export const { flags: stackFlags, args: stackArgs, withId: withStackId, } = new FlagSetBuilder("stack", "s", {
|
|
4
|
+
retrieveFunction: async (client, context) => {
|
|
5
|
+
const projectContext = await context.projectId();
|
|
6
|
+
if (!projectContext) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const projectId = projectContext.value;
|
|
10
|
+
const stacks = await client.container.listStacks({ projectId });
|
|
11
|
+
assertStatus(stacks, 200);
|
|
12
|
+
if (stacks.data.length === 1) {
|
|
13
|
+
return stacks.data[0].id;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
},
|
|
17
|
+
}).build();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface TemplateContent {
|
|
2
|
+
composeYaml: string;
|
|
3
|
+
envContent: string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare class TemplateFileNotFoundError extends Error {
|
|
6
|
+
constructor(url: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class GitHubRateLimitError extends Error {
|
|
9
|
+
constructor();
|
|
10
|
+
}
|
|
11
|
+
export declare class TemplateNetworkError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare function validateTemplateName(name: string): void;
|
|
15
|
+
export declare function templateNameToRepoName(templateName: string): string;
|
|
16
|
+
export declare function buildGitHubRawUrl(templateName: string, filename: string): string;
|
|
17
|
+
export declare function fetchTemplateFile(url: string): Promise<string>;
|
|
18
|
+
export declare function loadStackFromTemplate(templateName: string): Promise<TemplateContent>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
export class TemplateFileNotFoundError extends Error {
|
|
3
|
+
constructor(url) {
|
|
4
|
+
super(`File not found at ${url}`);
|
|
5
|
+
this.name = "TemplateFileNotFoundError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class GitHubRateLimitError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("GitHub API rate limit exceeded. Please try again later.");
|
|
11
|
+
this.name = "GitHubRateLimitError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class TemplateNetworkError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "TemplateNetworkError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function validateTemplateName(name) {
|
|
21
|
+
const validFormat = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;
|
|
22
|
+
if (!validFormat.test(name)) {
|
|
23
|
+
throw new Error(`Invalid template name format: '${name}'. Expected format: 'owner/name' (e.g., 'mittwald/n8n')`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function templateNameToRepoName(templateName) {
|
|
27
|
+
const [owner, name] = templateName.split("/");
|
|
28
|
+
return `${owner}/stack-template-${name}`;
|
|
29
|
+
}
|
|
30
|
+
export function buildGitHubRawUrl(templateName, filename) {
|
|
31
|
+
const repoName = templateNameToRepoName(templateName);
|
|
32
|
+
return `https://raw.githubusercontent.com/${repoName}/main/${filename}`;
|
|
33
|
+
}
|
|
34
|
+
function convertAxiosError(error, url) {
|
|
35
|
+
if (!axios.isAxiosError(error)) {
|
|
36
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
37
|
+
}
|
|
38
|
+
const axiosError = error;
|
|
39
|
+
if (axiosError.response?.status === 404) {
|
|
40
|
+
return new TemplateFileNotFoundError(url);
|
|
41
|
+
}
|
|
42
|
+
if (axiosError.response?.status === 403) {
|
|
43
|
+
return new GitHubRateLimitError();
|
|
44
|
+
}
|
|
45
|
+
if (axiosError.code === "ETIMEDOUT") {
|
|
46
|
+
return new TemplateNetworkError("Request timed out while fetching template from GitHub");
|
|
47
|
+
}
|
|
48
|
+
if (axiosError.code === "ECONNREFUSED" || axiosError.code === "ENOTFOUND") {
|
|
49
|
+
return new TemplateNetworkError("Network error: Unable to connect to GitHub. Please check your internet connection.");
|
|
50
|
+
}
|
|
51
|
+
return error;
|
|
52
|
+
}
|
|
53
|
+
export async function fetchTemplateFile(url) {
|
|
54
|
+
try {
|
|
55
|
+
const response = await axios.get(url, {
|
|
56
|
+
responseType: "text",
|
|
57
|
+
timeout: 10000,
|
|
58
|
+
});
|
|
59
|
+
return response.data;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw convertAxiosError(error, url);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function loadStackFromTemplate(templateName) {
|
|
66
|
+
validateTemplateName(templateName);
|
|
67
|
+
const composeUrl = buildGitHubRawUrl(templateName, "docker-compose.yml");
|
|
68
|
+
const envUrl = buildGitHubRawUrl(templateName, ".env");
|
|
69
|
+
let composeYaml;
|
|
70
|
+
try {
|
|
71
|
+
composeYaml = await fetchTemplateFile(composeUrl);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof TemplateFileNotFoundError) {
|
|
75
|
+
const repoName = templateNameToRepoName(templateName);
|
|
76
|
+
throw new Error(`Template '${templateName}' not found. Repository '${repoName}' does not exist or does not contain a docker-compose.yml file.`);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
let envContent = null;
|
|
81
|
+
try {
|
|
82
|
+
envContent = await fetchTemplateFile(envUrl);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof TemplateFileNotFoundError) {
|
|
86
|
+
// .env file is optional, so we ignore 404 errors
|
|
87
|
+
envContent = null;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { composeYaml, envContent };
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|