@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.
Files changed (83) hide show
  1. package/README.md +3 -0
  2. package/dist/commands/container/delete.d.ts +15 -0
  3. package/dist/commands/container/delete.js +39 -0
  4. package/dist/commands/container/list.d.ts +23 -0
  5. package/dist/commands/container/list.js +53 -0
  6. package/dist/commands/container/logs.d.ts +14 -0
  7. package/dist/commands/container/logs.js +47 -0
  8. package/dist/commands/container/recreate.d.ts +20 -0
  9. package/dist/commands/container/recreate.js +72 -0
  10. package/dist/commands/container/restart.d.ts +18 -0
  11. package/dist/commands/container/restart.js +40 -0
  12. package/dist/commands/container/run.d.ts +41 -0
  13. package/dist/commands/container/run.js +167 -0
  14. package/dist/commands/container/start.d.ts +18 -0
  15. package/dist/commands/container/start.js +40 -0
  16. package/dist/commands/container/stop.d.ts +18 -0
  17. package/dist/commands/container/stop.js +40 -0
  18. package/dist/commands/container/update.d.ts +36 -0
  19. package/dist/commands/container/update.js +174 -0
  20. package/dist/commands/context/set.d.ts +1 -0
  21. package/dist/commands/context/set.js +8 -0
  22. package/dist/commands/cronjob/execution/logs.js +6 -24
  23. package/dist/commands/cronjob/list.d.ts +2 -3
  24. package/dist/commands/domain/virtualhost/create.d.ts +1 -0
  25. package/dist/commands/domain/virtualhost/create.js +12 -0
  26. package/dist/commands/extension/install.js +4 -3
  27. package/dist/commands/extension/list-installed.js +4 -3
  28. package/dist/commands/mail/address/create.d.ts +2 -1
  29. package/dist/commands/mail/address/create.js +2 -1
  30. package/dist/commands/org/membership/list-own.d.ts +3 -0
  31. package/dist/commands/org/membership/list.d.ts +3 -0
  32. package/dist/commands/registry/create.d.ts +20 -0
  33. package/dist/commands/registry/create.js +77 -0
  34. package/dist/commands/registry/delete.d.ts +13 -0
  35. package/dist/commands/registry/delete.js +21 -0
  36. package/dist/commands/registry/list.d.ts +23 -0
  37. package/dist/commands/registry/list.js +33 -0
  38. package/dist/commands/registry/update.d.ts +20 -0
  39. package/dist/commands/registry/update.js +73 -0
  40. package/dist/commands/stack/delete.d.ts +16 -0
  41. package/dist/commands/stack/delete.js +54 -0
  42. package/dist/commands/stack/deploy.d.ts +18 -0
  43. package/dist/commands/stack/deploy.js +75 -0
  44. package/dist/commands/stack/list.d.ts +24 -0
  45. package/dist/commands/stack/list.js +60 -0
  46. package/dist/commands/stack/ps.d.ts +23 -0
  47. package/dist/commands/stack/ps.js +51 -0
  48. package/dist/lib/basecommands/DeleteBaseCommand.js +1 -1
  49. package/dist/lib/basecommands/ExtendedBaseCommand.d.ts +1 -0
  50. package/dist/lib/basecommands/ExtendedBaseCommand.js +4 -0
  51. package/dist/lib/context/Context.d.ts +5 -2
  52. package/dist/lib/context/Context.js +10 -1
  53. package/dist/lib/context/FlagSetBuilder.d.ts +3 -4
  54. package/dist/lib/context/FlagSetBuilder.js +22 -15
  55. package/dist/lib/resources/container/containerconfig.d.ts +43 -0
  56. package/dist/lib/resources/container/containerconfig.js +82 -0
  57. package/dist/lib/resources/container/flags.d.ts +13 -0
  58. package/dist/lib/resources/container/flags.js +34 -0
  59. package/dist/lib/resources/org/flags.js +7 -1
  60. package/dist/lib/resources/server/flags.js +7 -1
  61. package/dist/lib/resources/stack/enrich.d.ts +4 -0
  62. package/dist/lib/resources/stack/enrich.js +55 -0
  63. package/dist/lib/resources/stack/env.d.ts +1 -0
  64. package/dist/lib/resources/stack/env.js +11 -0
  65. package/dist/lib/resources/stack/flags.d.ts +5 -0
  66. package/dist/lib/resources/stack/flags.js +2 -0
  67. package/dist/lib/resources/stack/loader.d.ts +5 -0
  68. package/dist/lib/resources/stack/loader.js +47 -0
  69. package/dist/lib/resources/stack/loader.test.d.ts +1 -0
  70. package/dist/lib/resources/stack/loader.test.js +51 -0
  71. package/dist/lib/resources/stack/sanitize.d.ts +10 -0
  72. package/dist/lib/resources/stack/sanitize.js +43 -0
  73. package/dist/lib/resources/stack/types.d.ts +12 -0
  74. package/dist/lib/resources/stack/types.js +1 -0
  75. package/dist/lib/util/pager.d.ts +7 -0
  76. package/dist/lib/util/pager.js +21 -0
  77. package/dist/rendering/process/process.d.ts +1 -1
  78. package/dist/rendering/process/process_fancy.d.ts +1 -1
  79. package/dist/rendering/process/process_fancy.js +2 -1
  80. package/dist/rendering/process/process_flags.js +1 -1
  81. package/dist/rendering/process/process_quiet.d.ts +1 -1
  82. package/dist/rendering/process/process_quiet.js +3 -0
  83. 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 or short ID of ${article} ${displayName}`;
64
- let description = `May contain a short ID or a full ID of ${article} ${displayName}`;
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 or short ID of ${article} ${displayName}`;
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 != null) {
103
- const format = this.opts.expectedShortIDFormat;
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
- return () => { };
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", { normalize }).build();
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", { normalize }).build();
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,2 @@
1
+ import FlagSetBuilder from "../../context/FlagSetBuilder.js";
2
+ export const { flags: stackFlags, args: stackArgs, withId: withStackId, } = new FlagSetBuilder("stack", "s").build();
@@ -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,7 @@
1
+ /**
2
+ * Prints the given content to a pager, such as `less`. This function respects
3
+ * the `PAGER` environment variable.
4
+ *
5
+ * @param content
6
+ */
7
+ export declare function printToPager(content: string): void;
@@ -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 result = await fn();
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>;
@@ -11,6 +11,9 @@ export class SilentProcessRenderer {
11
11
  });
12
12
  }
13
13
  runStep(unusedTitle, fn) {
14
+ if (fn instanceof Promise) {
15
+ return fn;
16
+ }
14
17
  return fn();
15
18
  }
16
19
  addInfo() {