@mittwald/cli 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/commands/container/delete.d.ts +15 -0
- package/dist/commands/container/delete.js +39 -0
- package/dist/commands/container/list.d.ts +23 -0
- package/dist/commands/container/list.js +53 -0
- package/dist/commands/container/logs.d.ts +14 -0
- package/dist/commands/container/logs.js +47 -0
- package/dist/commands/container/recreate.d.ts +20 -0
- package/dist/commands/container/recreate.js +72 -0
- package/dist/commands/container/restart.d.ts +18 -0
- package/dist/commands/container/restart.js +40 -0
- package/dist/commands/container/run.d.ts +41 -0
- package/dist/commands/container/run.js +167 -0
- package/dist/commands/container/start.d.ts +18 -0
- package/dist/commands/container/start.js +40 -0
- package/dist/commands/container/stop.d.ts +18 -0
- package/dist/commands/container/stop.js +40 -0
- package/dist/commands/container/update.d.ts +36 -0
- package/dist/commands/container/update.js +174 -0
- package/dist/commands/context/set.d.ts +1 -0
- package/dist/commands/context/set.js +8 -0
- package/dist/commands/cronjob/execution/logs.js +6 -24
- package/dist/commands/cronjob/list.d.ts +2 -3
- package/dist/commands/domain/virtualhost/create.d.ts +1 -0
- package/dist/commands/domain/virtualhost/create.js +12 -0
- package/dist/commands/extension/install.js +4 -3
- package/dist/commands/extension/list-installed.js +4 -3
- package/dist/commands/mail/address/create.d.ts +2 -1
- package/dist/commands/mail/address/create.js +2 -1
- package/dist/commands/org/membership/list-own.d.ts +3 -0
- package/dist/commands/org/membership/list.d.ts +3 -0
- package/dist/commands/registry/create.d.ts +20 -0
- package/dist/commands/registry/create.js +77 -0
- package/dist/commands/registry/delete.d.ts +13 -0
- package/dist/commands/registry/delete.js +21 -0
- package/dist/commands/registry/list.d.ts +23 -0
- package/dist/commands/registry/list.js +33 -0
- package/dist/commands/registry/update.d.ts +20 -0
- package/dist/commands/registry/update.js +73 -0
- package/dist/commands/stack/delete.d.ts +16 -0
- package/dist/commands/stack/delete.js +54 -0
- package/dist/commands/stack/deploy.d.ts +18 -0
- package/dist/commands/stack/deploy.js +75 -0
- package/dist/commands/stack/list.d.ts +24 -0
- package/dist/commands/stack/list.js +60 -0
- package/dist/commands/stack/ps.d.ts +23 -0
- package/dist/commands/stack/ps.js +51 -0
- package/dist/lib/basecommands/DeleteBaseCommand.js +1 -1
- package/dist/lib/basecommands/ExtendedBaseCommand.d.ts +1 -0
- package/dist/lib/basecommands/ExtendedBaseCommand.js +4 -0
- package/dist/lib/context/Context.d.ts +5 -2
- package/dist/lib/context/Context.js +10 -1
- package/dist/lib/context/FlagSetBuilder.d.ts +3 -4
- package/dist/lib/context/FlagSetBuilder.js +22 -15
- package/dist/lib/resources/container/containerconfig.d.ts +43 -0
- package/dist/lib/resources/container/containerconfig.js +82 -0
- package/dist/lib/resources/container/flags.d.ts +13 -0
- package/dist/lib/resources/container/flags.js +34 -0
- package/dist/lib/resources/org/flags.js +7 -1
- package/dist/lib/resources/server/flags.js +7 -1
- package/dist/lib/resources/stack/enrich.d.ts +4 -0
- package/dist/lib/resources/stack/enrich.js +55 -0
- package/dist/lib/resources/stack/env.d.ts +1 -0
- package/dist/lib/resources/stack/env.js +11 -0
- package/dist/lib/resources/stack/flags.d.ts +5 -0
- package/dist/lib/resources/stack/flags.js +2 -0
- package/dist/lib/resources/stack/loader.d.ts +5 -0
- package/dist/lib/resources/stack/loader.js +47 -0
- package/dist/lib/resources/stack/loader.test.d.ts +1 -0
- package/dist/lib/resources/stack/loader.test.js +51 -0
- package/dist/lib/resources/stack/sanitize.d.ts +10 -0
- package/dist/lib/resources/stack/sanitize.js +43 -0
- package/dist/lib/resources/stack/types.d.ts +12 -0
- package/dist/lib/resources/stack/types.js +1 -0
- package/dist/lib/util/pager.d.ts +7 -0
- package/dist/lib/util/pager.js +21 -0
- package/dist/rendering/process/process.d.ts +1 -1
- package/dist/rendering/process/process_fancy.d.ts +1 -1
- package/dist/rendering/process/process_fancy.js +2 -1
- package/dist/rendering/process/process_flags.js +1 -1
- package/dist/rendering/process/process_quiet.d.ts +1 -1
- package/dist/rendering/process/process_quiet.js +3 -0
- package/package.json +21 -17
|
@@ -59,9 +59,13 @@ export default class FlagSetBuilder {
|
|
|
59
59
|
buildFlags() {
|
|
60
60
|
const { displayName } = this;
|
|
61
61
|
const article = articleForWord(displayName);
|
|
62
|
-
const { retrieveFromContext = true } = this.opts;
|
|
63
|
-
let summary = `ID
|
|
64
|
-
let description = `May contain a
|
|
62
|
+
const { retrieveFromContext = true, expectedShortIDFormat } = this.opts;
|
|
63
|
+
let summary = `ID of ${article} ${displayName}`;
|
|
64
|
+
let description = `May contain a ID of ${article} ${displayName}`;
|
|
65
|
+
if (expectedShortIDFormat) {
|
|
66
|
+
summary = `ID or short ID of ${article} ${displayName}`;
|
|
67
|
+
description = `May contain a short ID or a full ID of ${article} ${displayName}`;
|
|
68
|
+
}
|
|
65
69
|
if (retrieveFromContext) {
|
|
66
70
|
summary += `; this flag is optional if a default ${displayName} is set in the context`;
|
|
67
71
|
description += `; you can also use the "<%= config.bin %> context set --${this.flagName}=<VALUE>" command to persistently set a default ${displayName} for all commands that accept this flag.`;
|
|
@@ -83,8 +87,11 @@ export default class FlagSetBuilder {
|
|
|
83
87
|
buildArgs() {
|
|
84
88
|
const { displayName } = this;
|
|
85
89
|
const article = articleForWord(displayName);
|
|
86
|
-
const { retrieveFromContext = true } = this.opts;
|
|
87
|
-
let description = `ID
|
|
90
|
+
const { retrieveFromContext = true, expectedShortIDFormat } = this.opts;
|
|
91
|
+
let description = `ID of ${article} ${displayName}`;
|
|
92
|
+
if (expectedShortIDFormat) {
|
|
93
|
+
description = `ID or short ID of ${article} ${displayName}`;
|
|
94
|
+
}
|
|
88
95
|
if (retrieveFromContext) {
|
|
89
96
|
description += `; this argument is optional if a default ${displayName} is set in the context.`;
|
|
90
97
|
}
|
|
@@ -99,28 +106,28 @@ export default class FlagSetBuilder {
|
|
|
99
106
|
};
|
|
100
107
|
}
|
|
101
108
|
buildSanityCheck() {
|
|
102
|
-
if (this.opts.expectedShortIDFormat
|
|
103
|
-
|
|
104
|
-
return (id) => {
|
|
105
|
-
if (!validateUuid(id) && !format.pattern.test(id)) {
|
|
106
|
-
throw new UnexpectedShortIDPassedError(this.displayName, format.display);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
+
if (!this.opts.expectedShortIDFormat) {
|
|
110
|
+
return () => { };
|
|
109
111
|
}
|
|
110
|
-
|
|
112
|
+
const format = this.opts.expectedShortIDFormat;
|
|
113
|
+
return (id) => {
|
|
114
|
+
if (!validateUuid(id) && !format.pattern.test(id)) {
|
|
115
|
+
throw new UnexpectedShortIDPassedError(this.displayName, format.display);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
111
118
|
}
|
|
112
119
|
buildIDGetter() {
|
|
113
120
|
const idInputSanityCheck = this.buildSanityCheck();
|
|
114
121
|
const idFromArgsOrFlag = this.buildIDFromArgsOrFlag();
|
|
115
122
|
const { normalize = (_, id) => id, retrieveFromContext = true } = this.opts;
|
|
116
123
|
return async (apiClient, commandType, flags, args, cfg) => {
|
|
124
|
+
const context = new Context(apiClient, cfg);
|
|
117
125
|
const idInput = idFromArgsOrFlag(flags, args);
|
|
118
126
|
if (idInput) {
|
|
119
127
|
idInputSanityCheck(idInput);
|
|
120
|
-
return normalize(apiClient, idInput);
|
|
128
|
+
return normalize(apiClient, idInput, context);
|
|
121
129
|
}
|
|
122
130
|
if (retrieveFromContext) {
|
|
123
|
-
const context = new Context(apiClient, cfg);
|
|
124
131
|
const idFromContext = await context.getContextValue(this.flagName);
|
|
125
132
|
if (idFromContext) {
|
|
126
133
|
return idFromContext.value;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
|
+
type ContainerContainerImageConfig = MittwaldAPIV2.Components.Schemas.ContainerContainerImageConfig;
|
|
3
|
+
/**
|
|
4
|
+
* Parses environment variables from command line flags and env files
|
|
5
|
+
*
|
|
6
|
+
* @param envFlags Array of environment variable strings in KEY=VALUE format
|
|
7
|
+
* @param envFiles Array of paths to env files
|
|
8
|
+
* @returns An object containing environment variable key-value pairs
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseEnvironmentVariables(envFlags?: string[], envFiles?: string[]): Promise<Record<string, string>>;
|
|
11
|
+
/**
|
|
12
|
+
* Parses environment variables from env files
|
|
13
|
+
*
|
|
14
|
+
* @param envFiles Array of paths to env files
|
|
15
|
+
* @returns An object containing environment variable key-value pairs
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseEnvironmentVariablesFromFile(envFiles?: string[]): Promise<Record<string, string>>;
|
|
18
|
+
/**
|
|
19
|
+
* Parses environment variables from command line flags
|
|
20
|
+
*
|
|
21
|
+
* @param envFlags Array of environment variable strings in KEY=VALUE format
|
|
22
|
+
* @returns An object containing environment variable key-value pairs
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseEnvironmentVariablesFromEnvFlags(envFlags?: string[]): Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Determines which ports to expose based on image metadata
|
|
27
|
+
*
|
|
28
|
+
* @param imageMeta Metadata about the container image
|
|
29
|
+
* @param publishAll Whether to publish all ports defined in the image
|
|
30
|
+
* @param publishPorts Array of port mappings specified by the user
|
|
31
|
+
* @returns An array of port mappings
|
|
32
|
+
*/
|
|
33
|
+
export declare function getPortMappings(imageMeta: ContainerContainerImageConfig, publishAll?: boolean, publishPorts?: string[]): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves metadata for a container image
|
|
36
|
+
*
|
|
37
|
+
* @param apiClient The API client instance
|
|
38
|
+
* @param image The container image reference
|
|
39
|
+
* @param projectId The project ID for credentials
|
|
40
|
+
* @returns Metadata about the container image
|
|
41
|
+
*/
|
|
42
|
+
export declare function getImageMeta(apiClient: MittwaldAPIV2Client, image: string, projectId: string): Promise<ContainerContainerImageConfig>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { assertStatus, } from "@mittwald/api-client";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import { parse } from "envfile";
|
|
4
|
+
import { pathExists } from "../../util/fs/pathExists.js";
|
|
5
|
+
/**
|
|
6
|
+
* Parses environment variables from command line flags and env files
|
|
7
|
+
*
|
|
8
|
+
* @param envFlags Array of environment variable strings in KEY=VALUE format
|
|
9
|
+
* @param envFiles Array of paths to env files
|
|
10
|
+
* @returns An object containing environment variable key-value pairs
|
|
11
|
+
*/
|
|
12
|
+
export async function parseEnvironmentVariables(envFlags = [], envFiles = []) {
|
|
13
|
+
return {
|
|
14
|
+
...parseEnvironmentVariablesFromEnvFlags(envFlags),
|
|
15
|
+
...(await parseEnvironmentVariablesFromFile(envFiles)),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parses environment variables from env files
|
|
20
|
+
*
|
|
21
|
+
* @param envFiles Array of paths to env files
|
|
22
|
+
* @returns An object containing environment variable key-value pairs
|
|
23
|
+
*/
|
|
24
|
+
export async function parseEnvironmentVariablesFromFile(envFiles = []) {
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const envFile of envFiles) {
|
|
27
|
+
if (!(await pathExists(envFile))) {
|
|
28
|
+
throw new Error(`Env file not found: ${envFile}`);
|
|
29
|
+
}
|
|
30
|
+
const fileContent = await fs.readFile(envFile, { encoding: "utf-8" });
|
|
31
|
+
const parsed = parse(fileContent);
|
|
32
|
+
Object.assign(result, parsed);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parses environment variables from command line flags
|
|
38
|
+
*
|
|
39
|
+
* @param envFlags Array of environment variable strings in KEY=VALUE format
|
|
40
|
+
* @returns An object containing environment variable key-value pairs
|
|
41
|
+
*/
|
|
42
|
+
export function parseEnvironmentVariablesFromEnvFlags(envFlags = []) {
|
|
43
|
+
const splitIntoKeyAndValue = (e) => e.split("=", 2);
|
|
44
|
+
return Object.fromEntries(envFlags.map(splitIntoKeyAndValue));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Determines which ports to expose based on image metadata
|
|
48
|
+
*
|
|
49
|
+
* @param imageMeta Metadata about the container image
|
|
50
|
+
* @param publishAll Whether to publish all ports defined in the image
|
|
51
|
+
* @param publishPorts Array of port mappings specified by the user
|
|
52
|
+
* @returns An array of port mappings
|
|
53
|
+
*/
|
|
54
|
+
export function getPortMappings(imageMeta, publishAll = false, publishPorts = []) {
|
|
55
|
+
if (publishAll) {
|
|
56
|
+
const definedPorts = imageMeta.exposedPorts ?? [];
|
|
57
|
+
const concatPort = (p) => {
|
|
58
|
+
const [port, protocol = "tcp"] = p.port.split("/", 2);
|
|
59
|
+
return `${port}:${port}/${protocol}`;
|
|
60
|
+
};
|
|
61
|
+
return definedPorts.map(concatPort);
|
|
62
|
+
}
|
|
63
|
+
return publishPorts;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Retrieves metadata for a container image
|
|
67
|
+
*
|
|
68
|
+
* @param apiClient The API client instance
|
|
69
|
+
* @param image The container image reference
|
|
70
|
+
* @param projectId The project ID for credentials
|
|
71
|
+
* @returns Metadata about the container image
|
|
72
|
+
*/
|
|
73
|
+
export async function getImageMeta(apiClient, image, projectId) {
|
|
74
|
+
const resp = await apiClient.container.getContainerImageConfig({
|
|
75
|
+
queryParameters: {
|
|
76
|
+
imageReference: image,
|
|
77
|
+
useCredentialsForProjectId: projectId,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
assertStatus(resp, 200);
|
|
81
|
+
return resp.data;
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
|
+
import { CommandType } from "../../context/FlagSetBuilder.js";
|
|
3
|
+
import { Config } from "@oclif/core";
|
|
4
|
+
export declare const containerFlags: import("../../context/FlagSetBuilder.js").ContextFlags<"container">, containerArgs: import("../../context/FlagSetBuilder.js").ContextArgs<"container">, withContainerId: (apiClient: MittwaldAPIV2Client, command: "flag" | "arg" | CommandType<"container">, flags: {
|
|
5
|
+
[k: string]: unknown;
|
|
6
|
+
}, args: {
|
|
7
|
+
[k: string]: unknown;
|
|
8
|
+
}, cfg: Config) => Promise<string>;
|
|
9
|
+
export declare function withContainerAndStackId(apiClient: MittwaldAPIV2Client, command: CommandType<"project"> | "flag" | "arg", flags: {
|
|
10
|
+
[k: string]: unknown;
|
|
11
|
+
}, args: {
|
|
12
|
+
[k: string]: unknown;
|
|
13
|
+
}, cfg: Config): Promise<[string, string]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
2
|
+
import FlagSetBuilder from "../../context/FlagSetBuilder.js";
|
|
3
|
+
import { contextIDNormalizers } from "../../context/Context.js";
|
|
4
|
+
import { withProjectId } from "../project/flags.js";
|
|
5
|
+
async function normalize(apiClient, serviceId, ctx) {
|
|
6
|
+
const { value: stackId } = await ctx.mustGetContextValue("stack-id");
|
|
7
|
+
const result = await apiClient.container.getService({
|
|
8
|
+
stackId,
|
|
9
|
+
serviceId,
|
|
10
|
+
});
|
|
11
|
+
assertStatus(result, 200);
|
|
12
|
+
return result.data.id;
|
|
13
|
+
}
|
|
14
|
+
contextIDNormalizers["container-id"] = normalize;
|
|
15
|
+
export const { flags: containerFlags, args: containerArgs, withId: withContainerId, } = new FlagSetBuilder("container", "c", { normalize }).build();
|
|
16
|
+
export async function withContainerAndStackId(apiClient, command, flags, args, cfg) {
|
|
17
|
+
const projectId = await withProjectId(apiClient, command, flags, args, cfg);
|
|
18
|
+
const containerId = flags["container-id"] ?? args["container-id"];
|
|
19
|
+
if (typeof containerId !== "string") {
|
|
20
|
+
throw new Error("container ID, short ID or name must be specified");
|
|
21
|
+
}
|
|
22
|
+
const containerResp = await apiClient.container.listServices({
|
|
23
|
+
projectId,
|
|
24
|
+
});
|
|
25
|
+
assertStatus(containerResp, 200);
|
|
26
|
+
for (const container of containerResp.data) {
|
|
27
|
+
if (container.shortId === containerId ||
|
|
28
|
+
container.id === containerId ||
|
|
29
|
+
container.serviceName === containerId) {
|
|
30
|
+
return [container.id, container.stackId];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`no container ${containerId} found in project ${projectId}`);
|
|
34
|
+
}
|
|
@@ -7,4 +7,10 @@ async function normalize(apiClient, customerId) {
|
|
|
7
7
|
return customer.data.customerId;
|
|
8
8
|
}
|
|
9
9
|
contextIDNormalizers["org-id"] = normalize;
|
|
10
|
-
export const { flags: orgFlags, args: orgArgs, withId: withOrgId, } = new FlagSetBuilder("org", "o", {
|
|
10
|
+
export const { flags: orgFlags, args: orgArgs, withId: withOrgId, } = new FlagSetBuilder("org", "o", {
|
|
11
|
+
normalize,
|
|
12
|
+
expectedShortIDFormat: {
|
|
13
|
+
pattern: /^\d+$/,
|
|
14
|
+
display: "000000",
|
|
15
|
+
},
|
|
16
|
+
}).build();
|
|
@@ -7,4 +7,10 @@ async function normalize(apiClient, serverId) {
|
|
|
7
7
|
return result.data.id;
|
|
8
8
|
}
|
|
9
9
|
contextIDNormalizers["server-id"] = normalize;
|
|
10
|
-
export const { flags: serverFlags, args: serverArgs, withId: withServerId, } = new FlagSetBuilder("server", "s", {
|
|
10
|
+
export const { flags: serverFlags, args: serverArgs, withId: withServerId, } = new FlagSetBuilder("server", "s", {
|
|
11
|
+
normalize,
|
|
12
|
+
expectedShortIDFormat: {
|
|
13
|
+
pattern: /^s-.*/,
|
|
14
|
+
display: "s-XXXXXX",
|
|
15
|
+
},
|
|
16
|
+
}).build();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
|
+
type StackRequest = MittwaldAPIV2.Paths.V2StacksStackId.Put.Parameters.RequestBody;
|
|
3
|
+
export declare function enrichStackDefinition(apiClient: MittwaldAPIV2Client, projectId: string, input: StackRequest): Promise<StackRequest>;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { assertStatus, } from "@mittwald/api-client";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { parse } from "envfile";
|
|
4
|
+
export async function enrichStackDefinition(apiClient, projectId, input) {
|
|
5
|
+
const enriched = structuredClone(input);
|
|
6
|
+
for (const serviceName of Object.keys(input.services ?? {})) {
|
|
7
|
+
let service = enriched.services[serviceName];
|
|
8
|
+
service = await setCommandAndEntrypointFromImage(apiClient, projectId, service);
|
|
9
|
+
service = await setEnvironmentFromEnvFile(service);
|
|
10
|
+
enriched.services[serviceName] = service;
|
|
11
|
+
}
|
|
12
|
+
return enriched;
|
|
13
|
+
}
|
|
14
|
+
async function setEnvironmentFromEnvFile(service) {
|
|
15
|
+
if (!service.env_file) {
|
|
16
|
+
return service;
|
|
17
|
+
}
|
|
18
|
+
const enriched = structuredClone(service);
|
|
19
|
+
const envFileContent = await readFile(service.env_file, "utf-8");
|
|
20
|
+
const envVars = parse(envFileContent);
|
|
21
|
+
delete enriched.env_file;
|
|
22
|
+
enriched.envs = {
|
|
23
|
+
...envVars,
|
|
24
|
+
...(enriched.envs ?? {}),
|
|
25
|
+
};
|
|
26
|
+
return enriched;
|
|
27
|
+
}
|
|
28
|
+
async function setCommandAndEntrypointFromImage(apiClient, projectId, service) {
|
|
29
|
+
const enriched = structuredClone(service);
|
|
30
|
+
const resp = await apiClient.container.getContainerImageConfig({
|
|
31
|
+
queryParameters: {
|
|
32
|
+
imageReference: service.image,
|
|
33
|
+
useCredentialsForProjectId: projectId,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
assertStatus(resp, 200);
|
|
37
|
+
if (service.ports === undefined) {
|
|
38
|
+
enriched.ports = (resp.data.exposedPorts ?? []).map((p) => p.port);
|
|
39
|
+
}
|
|
40
|
+
if (service.command === undefined) {
|
|
41
|
+
let command = resp.data.command;
|
|
42
|
+
if (typeof command === "string") {
|
|
43
|
+
command = [command];
|
|
44
|
+
}
|
|
45
|
+
enriched.command = command;
|
|
46
|
+
}
|
|
47
|
+
if (service.entrypoint === undefined) {
|
|
48
|
+
let entrypoint = resp.data.entrypoint;
|
|
49
|
+
if (typeof entrypoint === "string") {
|
|
50
|
+
entrypoint = [entrypoint];
|
|
51
|
+
}
|
|
52
|
+
enriched.entrypoint = entrypoint;
|
|
53
|
+
}
|
|
54
|
+
return enriched;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function collectEnvironment(base: NodeJS.ProcessEnv, envFile: string): Promise<Record<string, string | undefined>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import { parse } from "envfile";
|
|
3
|
+
import { pathExists } from "../../util/fs/pathExists.js";
|
|
4
|
+
export async function collectEnvironment(base, envFile) {
|
|
5
|
+
if (!(await pathExists(envFile))) {
|
|
6
|
+
return base;
|
|
7
|
+
}
|
|
8
|
+
const defs = await fs.readFile(envFile, { encoding: "utf-8" });
|
|
9
|
+
const parsed = parse(defs);
|
|
10
|
+
return { ...base, ...parsed };
|
|
11
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const stackFlags: import("../../context/FlagSetBuilder.js").ContextFlags<"stack">, stackArgs: import("../../context/FlagSetBuilder.js").ContextArgs<"stack">, withStackId: (apiClient: import("@mittwald/api-client").MittwaldAPIV2Client, command: "flag" | "arg" | import("../../context/FlagSetBuilder.js").CommandType<"stack">, flags: {
|
|
2
|
+
[k: string]: unknown;
|
|
3
|
+
}, args: {
|
|
4
|
+
[k: string]: unknown;
|
|
5
|
+
}, cfg: import("@oclif/core").Config) => Promise<string>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MittwaldAPIV2 } from "@mittwald/api-client";
|
|
2
|
+
type StackRequest = MittwaldAPIV2.Paths.V2StacksStackId.Put.Parameters.RequestBody;
|
|
3
|
+
export declare function loadStackFromFile(file: string, environment: Record<string, string | undefined>): Promise<StackRequest>;
|
|
4
|
+
export declare function loadStackFromStr(input: string, environment: Record<string, string | undefined>): Promise<StackRequest>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import yaml from "js-yaml";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
function loadStackYAMLFromFile(file) {
|
|
4
|
+
if (file === "-") {
|
|
5
|
+
return fs.readFileSync(process.stdin.fd, { encoding: "utf-8" });
|
|
6
|
+
}
|
|
7
|
+
return fs.readFileSync(file, { encoding: "utf-8" });
|
|
8
|
+
}
|
|
9
|
+
export async function loadStackFromFile(file, environment) {
|
|
10
|
+
const input = loadStackYAMLFromFile(file);
|
|
11
|
+
return loadStackFromStr(input, environment);
|
|
12
|
+
}
|
|
13
|
+
export async function loadStackFromStr(input, environment) {
|
|
14
|
+
const stack = yaml.load(input);
|
|
15
|
+
const stackWithSubstitutedEnvironment = substituteEnvironmentVariables(stack, environment);
|
|
16
|
+
return stackWithSubstitutedEnvironment;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* This function aims to be a (more-or-less) complete implementation of Docker
|
|
20
|
+
* Compose's variable interpolation feature.
|
|
21
|
+
*
|
|
22
|
+
* @param input Input object; may be anything coming out of a YAML file.
|
|
23
|
+
* @param environment A record of environment variables
|
|
24
|
+
* @see https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/
|
|
25
|
+
*/
|
|
26
|
+
function substituteEnvironmentVariables(input, environment) {
|
|
27
|
+
if (typeof input === "string") {
|
|
28
|
+
return input.replace(/\$\{(\w+)(:?-(.*))?}/g, (_, key, defExpr, def = "") => {
|
|
29
|
+
const useDefaultIfNonEmpty = defExpr && defExpr.startsWith(":");
|
|
30
|
+
if (useDefaultIfNonEmpty) {
|
|
31
|
+
return environment[key] || def;
|
|
32
|
+
}
|
|
33
|
+
return key in environment ? environment[key] : def;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(input)) {
|
|
37
|
+
return input.map((item) => substituteEnvironmentVariables(item, environment));
|
|
38
|
+
}
|
|
39
|
+
if (input && typeof input === "object") {
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const [key, value] of Object.entries(input)) {
|
|
42
|
+
result[key] = substituteEnvironmentVariables(value, environment);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return input;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { loadStackFromStr } from "./loader.js";
|
|
3
|
+
describe("loadStackFromStr", () => {
|
|
4
|
+
it("should convert a stack YAML input to a stack definition", async () => {
|
|
5
|
+
const input = `services:
|
|
6
|
+
nginx:
|
|
7
|
+
image: nginx
|
|
8
|
+
ports:
|
|
9
|
+
- 80:80
|
|
10
|
+
environment:
|
|
11
|
+
FOO: bar
|
|
12
|
+
volumes:
|
|
13
|
+
data:/var/www
|
|
14
|
+
volumes:
|
|
15
|
+
data:`;
|
|
16
|
+
const stack = await loadStackFromStr(input, {});
|
|
17
|
+
expect(stack).toHaveProperty("services.nginx.image", "nginx");
|
|
18
|
+
});
|
|
19
|
+
it("interpolates environment variables in a stack definition", async () => {
|
|
20
|
+
const input = `services:
|
|
21
|
+
nginx:
|
|
22
|
+
image: nginx:\${NGINX_VERSION}`;
|
|
23
|
+
const stack = await loadStackFromStr(input, { NGINX_VERSION: "0.1.0" });
|
|
24
|
+
expect(stack).toHaveProperty("services.nginx.image", "nginx:0.1.0");
|
|
25
|
+
});
|
|
26
|
+
it("interpolates environment variables with default values", async () => {
|
|
27
|
+
const input = `services:
|
|
28
|
+
nginx:
|
|
29
|
+
image: nginx:\${NGINX_VERSION:-latest}`;
|
|
30
|
+
const stack = await loadStackFromStr(input, {});
|
|
31
|
+
expect(stack).toHaveProperty("services.nginx.image", "nginx:latest");
|
|
32
|
+
});
|
|
33
|
+
it("interpolates environment variables with default values for non-empty variables", async () => {
|
|
34
|
+
const input = `services:
|
|
35
|
+
nginx:
|
|
36
|
+
image: nginx
|
|
37
|
+
environment:
|
|
38
|
+
FOO: \${FOO:-foo}`;
|
|
39
|
+
const stack = await loadStackFromStr(input, { FOO: "" });
|
|
40
|
+
expect(stack).toHaveProperty("services.nginx.environment.FOO", "foo");
|
|
41
|
+
});
|
|
42
|
+
it("interpolates environment variables with default values for empty variables", async () => {
|
|
43
|
+
const input = `services:
|
|
44
|
+
nginx:
|
|
45
|
+
image: nginx
|
|
46
|
+
environment:
|
|
47
|
+
FOO: \${FOO-foo}`;
|
|
48
|
+
const stack = await loadStackFromStr(input, { FOO: "" });
|
|
49
|
+
expect(stack).toHaveProperty("services.nginx.environment.FOO", "");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MittwaldAPIV2 } from "@mittwald/api-client";
|
|
2
|
+
type StackRequest = MittwaldAPIV2.Paths.V2StacksStackId.Put.Parameters.RequestBody;
|
|
3
|
+
/**
|
|
4
|
+
* This function is needed to work around the mStudios supposedly docker-compose
|
|
5
|
+
* compatible container stack API.
|
|
6
|
+
*
|
|
7
|
+
* @param stack
|
|
8
|
+
*/
|
|
9
|
+
export declare function sanitizeStackDefinition(stack: StackRequest): StackRequest;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function is needed to work around the mStudios supposedly docker-compose
|
|
3
|
+
* compatible container stack API.
|
|
4
|
+
*
|
|
5
|
+
* @param stack
|
|
6
|
+
*/
|
|
7
|
+
export function sanitizeStackDefinition(stack) {
|
|
8
|
+
const sanitized = {
|
|
9
|
+
services: structuredClone(stack.services),
|
|
10
|
+
volumes: structuredClone(stack.volumes),
|
|
11
|
+
};
|
|
12
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
13
|
+
for (const serviceKey of Object.keys(sanitized.services ?? {})) {
|
|
14
|
+
const service = sanitized.services[serviceKey];
|
|
15
|
+
// mStudio requires "command" to be a string[]; in a docker-compose file, it
|
|
16
|
+
// can also be a regular string.
|
|
17
|
+
if (typeof service.command === "string") {
|
|
18
|
+
service.command = [service.command];
|
|
19
|
+
}
|
|
20
|
+
// descriptions are required for mStudio containers; docker compose has no
|
|
21
|
+
// equivalent field.
|
|
22
|
+
if (!service.description) {
|
|
23
|
+
service.description = serviceKey;
|
|
24
|
+
}
|
|
25
|
+
// mStudio calls it "envs", docker compose calls it "environment" 🫠
|
|
26
|
+
const serviceWithEnvironment = service;
|
|
27
|
+
if (serviceWithEnvironment.environment) {
|
|
28
|
+
service.envs = serviceWithEnvironment.environment;
|
|
29
|
+
delete serviceWithEnvironment.environment;
|
|
30
|
+
}
|
|
31
|
+
if (!service.envs) {
|
|
32
|
+
service.envs = {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// For mStudio, volume definitions must be empty objects, null is apparently
|
|
36
|
+
// not allowed.
|
|
37
|
+
for (const volumeKey of Object.keys(sanitized.volumes ?? {})) {
|
|
38
|
+
if (sanitized.volumes[volumeKey] === null) {
|
|
39
|
+
sanitized.volumes[volumeKey] = { name: volumeKey };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return sanitized;
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MittwaldAPIV2 } from "@mittwald/api-client";
|
|
2
|
+
type ContainerServiceDeclareRequest = MittwaldAPIV2.Components.Schemas.ContainerServiceDeclareRequest;
|
|
3
|
+
export type ContainerServiceInput = ContainerServiceDeclareRequest & {
|
|
4
|
+
command?: string[] | string;
|
|
5
|
+
entrypoint?: string[] | string;
|
|
6
|
+
env_file?: string;
|
|
7
|
+
environment?: {
|
|
8
|
+
[k: string]: string;
|
|
9
|
+
};
|
|
10
|
+
ports?: string[];
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import tempfile from "tempfile";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import cp from "child_process";
|
|
4
|
+
/**
|
|
5
|
+
* Prints the given content to a pager, such as `less`. This function respects
|
|
6
|
+
* the `PAGER` environment variable.
|
|
7
|
+
*
|
|
8
|
+
* @param content
|
|
9
|
+
*/
|
|
10
|
+
export function printToPager(content) {
|
|
11
|
+
const t = tempfile();
|
|
12
|
+
try {
|
|
13
|
+
fs.writeFileSync(t, content, { encoding: "utf8" });
|
|
14
|
+
cp.spawnSync(process.env.PAGER || "less", [t], {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
fs.unlinkSync(t);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -54,7 +54,7 @@ export declare class RunnableHandler {
|
|
|
54
54
|
export interface ProcessRenderer {
|
|
55
55
|
start(): void;
|
|
56
56
|
addStep(title: ReactNode): RunnableHandler;
|
|
57
|
-
runStep<TRes>(title: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
|
|
57
|
+
runStep<TRes>(title: ReactNode, fn: (() => Promise<TRes>) | Promise<TRes>): Promise<TRes>;
|
|
58
58
|
addInfo(title: ReactNode): void;
|
|
59
59
|
addConfirmation(question: ReactNode): Promise<boolean>;
|
|
60
60
|
addInput(question: ReactNode, mask?: boolean): Promise<string>;
|
|
@@ -8,7 +8,7 @@ export declare class FancyProcessRenderer implements ProcessRenderer {
|
|
|
8
8
|
constructor(title: string);
|
|
9
9
|
start(): void;
|
|
10
10
|
addStep(title: ReactNode): RunnableHandler;
|
|
11
|
-
runStep<TRes>(title: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
|
|
11
|
+
runStep<TRes>(title: ReactNode, fn: (() => Promise<TRes>) | Promise<TRes>): Promise<TRes>;
|
|
12
12
|
addInfo(title: ReactNode): void;
|
|
13
13
|
addInput(question: React.ReactElement, mask?: boolean): Promise<string>;
|
|
14
14
|
addSelect<TVal>(question: React.ReactNode, options: {
|
|
@@ -40,7 +40,8 @@ export class FancyProcessRenderer {
|
|
|
40
40
|
async runStep(title, fn) {
|
|
41
41
|
const step = this.addStep(title);
|
|
42
42
|
try {
|
|
43
|
-
const
|
|
43
|
+
const promise = typeof fn === "function" ? fn() : fn;
|
|
44
|
+
const result = await promise;
|
|
44
45
|
step.complete();
|
|
45
46
|
return result;
|
|
46
47
|
}
|
|
@@ -4,7 +4,7 @@ import { SilentProcessRenderer } from "./process_quiet.js";
|
|
|
4
4
|
export const processFlags = {
|
|
5
5
|
quiet: Flags.boolean({
|
|
6
6
|
char: "q",
|
|
7
|
-
summary: "suppress process output and only display a machine-readable summary
|
|
7
|
+
summary: "suppress process output and only display a machine-readable summary",
|
|
8
8
|
description: "This flag controls if you want to see the process output or only a summary. When using <%= config.bin %> non-interactively (e.g. in scripts), you can use this flag to easily get the IDs of created resources for further processing.",
|
|
9
9
|
}),
|
|
10
10
|
};
|
|
@@ -4,7 +4,7 @@ export declare class SilentProcessRenderer implements ProcessRenderer {
|
|
|
4
4
|
private cleanupFns;
|
|
5
5
|
start(): void;
|
|
6
6
|
addStep(title: ReactNode): RunnableHandler;
|
|
7
|
-
runStep<TRes>(unusedTitle: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
|
|
7
|
+
runStep<TRes>(unusedTitle: ReactNode, fn: (() => Promise<TRes>) | Promise<TRes>): Promise<TRes>;
|
|
8
8
|
addInfo(): void;
|
|
9
9
|
complete(): Promise<void>;
|
|
10
10
|
error(err: unknown): Promise<void>;
|