@mittwald/cli 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) 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 +57 -0
  13. package/dist/commands/container/run.js +224 -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/context/set.d.ts +1 -0
  19. package/dist/commands/context/set.js +8 -0
  20. package/dist/commands/cronjob/execution/logs.js +6 -24
  21. package/dist/commands/cronjob/list.d.ts +2 -3
  22. package/dist/commands/domain/virtualhost/create.d.ts +1 -0
  23. package/dist/commands/domain/virtualhost/create.js +12 -0
  24. package/dist/commands/extension/install.js +4 -3
  25. package/dist/commands/extension/list-installed.js +4 -3
  26. package/dist/commands/registry/create.d.ts +20 -0
  27. package/dist/commands/registry/create.js +77 -0
  28. package/dist/commands/registry/delete.d.ts +13 -0
  29. package/dist/commands/registry/delete.js +21 -0
  30. package/dist/commands/registry/list.d.ts +23 -0
  31. package/dist/commands/registry/list.js +33 -0
  32. package/dist/commands/registry/update.d.ts +20 -0
  33. package/dist/commands/registry/update.js +73 -0
  34. package/dist/commands/stack/delete.d.ts +16 -0
  35. package/dist/commands/stack/delete.js +54 -0
  36. package/dist/commands/stack/deploy.d.ts +18 -0
  37. package/dist/commands/stack/deploy.js +75 -0
  38. package/dist/commands/stack/list.d.ts +24 -0
  39. package/dist/commands/stack/list.js +60 -0
  40. package/dist/commands/stack/ps.d.ts +23 -0
  41. package/dist/commands/stack/ps.js +51 -0
  42. package/dist/lib/basecommands/DeleteBaseCommand.js +1 -1
  43. package/dist/lib/basecommands/ExtendedBaseCommand.d.ts +1 -0
  44. package/dist/lib/basecommands/ExtendedBaseCommand.js +4 -0
  45. package/dist/lib/context/Context.d.ts +5 -2
  46. package/dist/lib/context/Context.js +10 -1
  47. package/dist/lib/context/FlagSetBuilder.d.ts +3 -4
  48. package/dist/lib/context/FlagSetBuilder.js +22 -15
  49. package/dist/lib/resources/container/flags.d.ts +13 -0
  50. package/dist/lib/resources/container/flags.js +34 -0
  51. package/dist/lib/resources/org/flags.js +7 -1
  52. package/dist/lib/resources/server/flags.js +7 -1
  53. package/dist/lib/resources/stack/enrich.d.ts +4 -0
  54. package/dist/lib/resources/stack/enrich.js +55 -0
  55. package/dist/lib/resources/stack/env.d.ts +1 -0
  56. package/dist/lib/resources/stack/env.js +11 -0
  57. package/dist/lib/resources/stack/flags.d.ts +5 -0
  58. package/dist/lib/resources/stack/flags.js +2 -0
  59. package/dist/lib/resources/stack/loader.d.ts +5 -0
  60. package/dist/lib/resources/stack/loader.js +47 -0
  61. package/dist/lib/resources/stack/loader.test.d.ts +1 -0
  62. package/dist/lib/resources/stack/loader.test.js +51 -0
  63. package/dist/lib/resources/stack/sanitize.d.ts +10 -0
  64. package/dist/lib/resources/stack/sanitize.js +43 -0
  65. package/dist/lib/resources/stack/types.d.ts +12 -0
  66. package/dist/lib/resources/stack/types.js +1 -0
  67. package/dist/lib/util/pager.d.ts +7 -0
  68. package/dist/lib/util/pager.js +21 -0
  69. package/dist/rendering/process/process.d.ts +1 -1
  70. package/dist/rendering/process/process_fancy.d.ts +1 -1
  71. package/dist/rendering/process/process_fancy.js +2 -1
  72. package/dist/rendering/process/process_flags.js +1 -1
  73. package/dist/rendering/process/process_quiet.d.ts +1 -1
  74. package/dist/rendering/process/process_quiet.js +3 -0
  75. package/package.json +21 -17
@@ -0,0 +1,54 @@
1
+ import { DeleteBaseCommand } from "../../lib/basecommands/DeleteBaseCommand.js";
2
+ import assertSuccess from "../../lib/apiutil/assert_success.js";
3
+ import { stackArgs } from "../../lib/resources/stack/flags.js";
4
+ import { assertStatus } from "@mittwald/api-client";
5
+ import { Flags } from "@oclif/core";
6
+ export default class Delete extends DeleteBaseCommand {
7
+ static description = "Delete a container stack";
8
+ static resourceName = "container stack";
9
+ static aliases = ["stack:rm"];
10
+ static flags = {
11
+ ...DeleteBaseCommand.baseFlags,
12
+ "with-volumes": Flags.boolean({
13
+ summary: "also include remove volumes in removal",
14
+ default: false,
15
+ char: "v",
16
+ }),
17
+ };
18
+ static args = { ...stackArgs };
19
+ async deleteResource() {
20
+ const stackId = await this.withStackId(Delete);
21
+ const stackResponse = await this.apiClient.container.getStack({ stackId });
22
+ assertStatus(stackResponse, 200);
23
+ if (stackResponse.data.projectId !== stackResponse.data.id) {
24
+ throw new Error("not implemented");
25
+ }
26
+ const resp = await this.apiClient.container.declareStack({
27
+ stackId,
28
+ data: {
29
+ services: {},
30
+ volumes: {},
31
+ },
32
+ });
33
+ assertSuccess(resp);
34
+ if (this.flags["with-volumes"]) {
35
+ await this.deleteVolumes(stackId, stackResponse.data.projectId);
36
+ }
37
+ }
38
+ async deleteVolumes(stackId, projectId) {
39
+ const volumesResponse = await this.apiClient.container.listVolumes({
40
+ projectId,
41
+ });
42
+ assertStatus(volumesResponse, 200);
43
+ for (const volume of volumesResponse.data) {
44
+ if (volume.stackId !== stackId) {
45
+ continue;
46
+ }
47
+ const deleteVolumeResponse = await this.apiClient.container.deleteVolume({
48
+ stackId: stackId,
49
+ volumeId: volume.id,
50
+ });
51
+ assertSuccess(deleteVolumeResponse);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,18 @@
1
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
2
+ import { ReactNode } from "react";
3
+ interface DeployResult {
4
+ restartedServices: string[];
5
+ }
6
+ export declare class Deploy extends ExecRenderBaseCommand<typeof Deploy, DeployResult> {
7
+ static description: string;
8
+ static aliases: string[];
9
+ static flags: {
10
+ "compose-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ "env-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ "stack-id": import("@oclif/core/interfaces").OptionFlag<string>;
14
+ };
15
+ protected exec(): Promise<DeployResult>;
16
+ protected render({ restartedServices }: DeployResult): ReactNode;
17
+ }
18
+ export {};
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
3
+ import { stackFlags, withStackId } from "../../lib/resources/stack/flags.js";
4
+ import { Flags } from "@oclif/core";
5
+ import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
6
+ import { loadStackFromFile } from "../../lib/resources/stack/loader.js";
7
+ import { assertStatus } from "@mittwald/api-client";
8
+ import assertSuccess from "../../lib/apiutil/assert_success.js";
9
+ import { collectEnvironment } from "../../lib/resources/stack/env.js";
10
+ import { sanitizeStackDefinition } from "../../lib/resources/stack/sanitize.js";
11
+ import { enrichStackDefinition } from "../../lib/resources/stack/enrich.js";
12
+ import { Success } from "../../rendering/react/components/Success.js";
13
+ import { Value } from "../../rendering/react/components/Value.js";
14
+ export class Deploy extends ExecRenderBaseCommand {
15
+ static description = "Deploys a docker-compose compatible file to a mittwald container stack";
16
+ static aliases = ["stack:up"];
17
+ static flags = {
18
+ ...stackFlags,
19
+ ...processFlags,
20
+ "compose-file": Flags.string({
21
+ summary: 'path to a compose file, or "-" to read from stdin',
22
+ default: "./docker-compose.yml",
23
+ char: "c",
24
+ }),
25
+ "env-file": Flags.file({
26
+ summary: "alternative path to file with environment variables",
27
+ default: "./.env",
28
+ }),
29
+ };
30
+ async exec() {
31
+ const stackId = await withStackId(this.apiClient, Deploy, this.flags, this.args, this.config);
32
+ const { "compose-file": composeFile, "env-file": envFile } = this.flags;
33
+ const r = makeProcessRenderer(this.flags, "Deploying container stack");
34
+ const result = { restartedServices: [] };
35
+ const stack = await r.runStep("getting stack", async () => {
36
+ const resp = await this.apiClient.container.getStack({ stackId });
37
+ assertStatus(resp, 200);
38
+ return resp.data;
39
+ });
40
+ const env = await collectEnvironment(process.env, envFile);
41
+ let stackDefinition = await loadStackFromFile(composeFile, env);
42
+ stackDefinition = sanitizeStackDefinition(stackDefinition);
43
+ stackDefinition = await r.runStep("getting image configurations", async () => {
44
+ return enrichStackDefinition(this.apiClient, stack.projectId, stackDefinition);
45
+ });
46
+ this.debug("complete stack definition: %O", stackDefinition);
47
+ const declaredStack = await r.runStep("deploying stack", async () => {
48
+ const resp = await this.apiClient.container.declareStack({
49
+ stackId,
50
+ data: stackDefinition,
51
+ });
52
+ assertStatus(resp, 200);
53
+ return resp.data;
54
+ });
55
+ for (const service of declaredStack.services ?? []) {
56
+ if (service.requiresRecreate) {
57
+ await r.runStep(`recreating service ${service.serviceName}`, async () => {
58
+ const resp = await this.apiClient.container.recreateService({
59
+ stackId,
60
+ serviceId: service.id,
61
+ });
62
+ assertSuccess(resp);
63
+ result.restartedServices.push(service.serviceName);
64
+ });
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+ render({ restartedServices }) {
70
+ if (restartedServices.length === 0) {
71
+ return (_jsx(Success, { children: "Deployment successful. No services were restarted." }));
72
+ }
73
+ return (_jsxs(Success, { children: ["Deployment successful. The following services were restarted:", " ", _jsx(Value, { children: restartedServices.join(", ") })] }));
74
+ }
75
+ }
@@ -0,0 +1,24 @@
1
+ import { Response, Simplify } from "@mittwald/api-client-commons";
2
+ import { type MittwaldAPIV2 } from "@mittwald/api-client";
3
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
4
+ import { ListColumns } from "../../rendering/formatter/Table.js";
5
+ type Stack = MittwaldAPIV2.Components.Schemas.ContainerStackResponse;
6
+ type ListResponse = Response<Stack[]>;
7
+ type ListItem = Simplify<Stack>;
8
+ export declare class List extends ListBaseCommand<typeof List, ListItem, ListResponse> {
9
+ static description: string;
10
+ static aliases: string[];
11
+ static args: {};
12
+ static flags: {
13
+ "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
14
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
15
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
20
+ };
21
+ getData(): Promise<ListResponse>;
22
+ protected getColumns(data: ListItem[]): ListColumns<ListItem>;
23
+ }
24
+ export {};
@@ -0,0 +1,60 @@
1
+ import { assertStatus } from "@mittwald/api-client";
2
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
3
+ import { projectFlags } from "../../lib/resources/project/flags.js";
4
+ export class List extends ListBaseCommand {
5
+ static description = "List container stacks for a given project.";
6
+ static aliases = ["stack:ls"];
7
+ static args = {};
8
+ static flags = {
9
+ ...ListBaseCommand.baseFlags,
10
+ ...projectFlags,
11
+ };
12
+ async getData() {
13
+ const projectId = await this.withProjectId(List);
14
+ const response = await this.apiClient.container.listStacks({
15
+ projectId,
16
+ });
17
+ assertStatus(response, 200);
18
+ return response;
19
+ }
20
+ getColumns(data) {
21
+ const { id } = super.getColumns(data);
22
+ return {
23
+ id,
24
+ description: {},
25
+ services: {
26
+ get(stack) {
27
+ if (stack.services === undefined || stack.services.length === 0) {
28
+ return "no services";
29
+ }
30
+ const countByStatus = {};
31
+ let requireRecreate = 0;
32
+ for (const service of stack.services || []) {
33
+ countByStatus[service.status] =
34
+ (countByStatus[service.status] || 0) + 1;
35
+ if (service.requiresRecreate) {
36
+ requireRecreate++;
37
+ }
38
+ }
39
+ const summaryItems = [];
40
+ for (const status of Object.keys(countByStatus)) {
41
+ summaryItems.push(countByStatus[status] + " " + status);
42
+ }
43
+ let summary = summaryItems.join(", ");
44
+ if (requireRecreate > 0) {
45
+ summary += ` (${requireRecreate} require recreation)`;
46
+ }
47
+ return summary;
48
+ },
49
+ },
50
+ volumes: {
51
+ get(stack) {
52
+ if (stack.volumes === undefined || stack.volumes.length === 0) {
53
+ return "no volumes";
54
+ }
55
+ return stack.volumes.length + " volumes";
56
+ },
57
+ },
58
+ };
59
+ }
60
+ }
@@ -0,0 +1,23 @@
1
+ import { Response, Simplify } from "@mittwald/api-client-commons";
2
+ import { type MittwaldAPIV2 } from "@mittwald/api-client";
3
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
4
+ import { ListColumns } from "../../rendering/formatter/Table.js";
5
+ type ContainerServiceResponse = MittwaldAPIV2.Components.Schemas.ContainerServiceResponse;
6
+ type ListResponse = Response<ContainerServiceResponse[]>;
7
+ type ListItem = Simplify<ContainerServiceResponse>;
8
+ export declare class ListContainers extends ListBaseCommand<typeof ListContainers, ListItem, ListResponse> {
9
+ static description: string;
10
+ static args: {};
11
+ static flags: {
12
+ "stack-id": import("@oclif/core/interfaces").OptionFlag<string>;
13
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
14
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
19
+ };
20
+ getData(): Promise<ListItem[]>;
21
+ protected getColumns(data: ListItem[]): ListColumns<ListItem>;
22
+ }
23
+ export {};
@@ -0,0 +1,51 @@
1
+ import { assertStatus } from "@mittwald/api-client";
2
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
3
+ import { stackFlags } from "../../lib/resources/stack/flags.js";
4
+ import { makeDateRendererForFormat } from "../../rendering/textformat/formatDate.js";
5
+ export class ListContainers extends ListBaseCommand {
6
+ static description = "List all services within a given container stack.";
7
+ static args = {};
8
+ static flags = {
9
+ ...ListBaseCommand.baseFlags,
10
+ ...stackFlags,
11
+ };
12
+ async getData() {
13
+ const stackId = await this.withStackId(ListContainers);
14
+ const response = await this.apiClient.container.getStack({
15
+ stackId,
16
+ });
17
+ assertStatus(response, 200);
18
+ return response.data.services || [];
19
+ }
20
+ getColumns(data) {
21
+ const { id } = super.getColumns(data);
22
+ const dateFormatter = makeDateRendererForFormat(this.flags.output, !this.flags["no-relative-dates"]);
23
+ return {
24
+ id,
25
+ name: {
26
+ get: (svc) => svc.serviceName,
27
+ },
28
+ image: {
29
+ get: (svc) => svc.deployedState.image,
30
+ },
31
+ command: {
32
+ get: (svc) => svc.deployedState.command?.join(" "),
33
+ },
34
+ description: {},
35
+ ports: {
36
+ get: (svc) => {
37
+ if (svc.deployedState.ports === undefined ||
38
+ svc.deployedState.ports.length === 0) {
39
+ return "no ports";
40
+ }
41
+ return svc.deployedState.ports.join(", ");
42
+ },
43
+ },
44
+ status: {
45
+ get(svc) {
46
+ return svc.status + " (" + dateFormatter(svc.statusSetAt) + ")";
47
+ },
48
+ },
49
+ };
50
+ }
51
+ }
@@ -10,7 +10,7 @@ export class DeleteBaseCommand extends ExecRenderBaseCommand {
10
10
  ...processFlags,
11
11
  force: Flags.boolean({
12
12
  char: "f",
13
- description: "Do not ask for confirmation",
13
+ description: "do not ask for confirmation",
14
14
  }),
15
15
  };
16
16
  async exec() {
@@ -8,4 +8,5 @@ export declare abstract class ExtendedBaseCommand<T extends typeof BaseCommand>
8
8
  withAppInstallationId(command: CommandType<"installation"> | "flag" | "arg"): Promise<string>;
9
9
  withProjectId(command: CommandType<"project"> | "flag" | "arg"): Promise<string>;
10
10
  withServerId(command: CommandType<"server"> | "flag" | "arg"): Promise<string>;
11
+ withStackId(command: CommandType<"stack"> | "flag" | "arg"): Promise<string>;
11
12
  }
@@ -2,6 +2,7 @@ import { BaseCommand } from "./BaseCommand.js";
2
2
  import { withAppInstallationId } from "../resources/app/flags.js";
3
3
  import { withProjectId } from "../resources/project/flags.js";
4
4
  import { withServerId } from "../resources/server/flags.js";
5
+ import { withStackId } from "../resources/stack/flags.js";
5
6
  export class ExtendedBaseCommand extends BaseCommand {
6
7
  flags;
7
8
  args;
@@ -25,4 +26,7 @@ export class ExtendedBaseCommand extends BaseCommand {
25
26
  async withServerId(command) {
26
27
  return withServerId(this.apiClient, command, this.flags, this.args, this.config);
27
28
  }
29
+ async withStackId(command) {
30
+ return withStackId(this.apiClient, command, this.flags, this.args, this.config);
31
+ }
28
32
  }
@@ -1,7 +1,7 @@
1
1
  import { Config } from "@oclif/core";
2
2
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
3
3
  import ContextProvider from "./ContextProvider.js";
4
- export type ContextNames = "project" | "server" | "org" | "installation" | "domain" | "dnszone" | "mailaddress" | "maildeliverybox" | "conversation" | "backup";
4
+ export type ContextNames = "project" | "server" | "org" | "installation" | "domain" | "dnszone" | "mailaddress" | "maildeliverybox" | "conversation" | "backup" | "stack" | "container";
5
5
  export type ContextKey<N extends ContextNames = ContextNames> = `${N}-id`;
6
6
  export type ContextMap = Partial<Record<ContextKey, ContextValue>>;
7
7
  export type ContextMapUpdate = Partial<Record<ContextKey, string>>;
@@ -14,7 +14,7 @@ export type ContextValue = {
14
14
  source: ContextValueSource;
15
15
  };
16
16
  export declare const contextIDNormalizers: {
17
- [k in ContextKey]?: (apiClient: MittwaldAPIV2Client, id: string) => Promise<string>;
17
+ [k in ContextKey]?: (apiClient: MittwaldAPIV2Client, id: string, ctx: Context) => Promise<string>;
18
18
  };
19
19
  export interface ContextOptions {
20
20
  onInitError: (err: unknown) => void;
@@ -29,13 +29,16 @@ export default class Context {
29
29
  reset(): Promise<void>;
30
30
  private persist;
31
31
  private setContextValue;
32
+ mustGetContextValue(key: ContextKey): Promise<ContextValue>;
32
33
  getContextValue(key: ContextKey): Promise<ContextValue | undefined>;
33
34
  setProjectId: (id: string) => Promise<string>;
34
35
  setServerId: (id: string) => Promise<string>;
35
36
  setOrgId: (id: string) => Promise<string>;
36
37
  setAppInstallationId: (id: string) => Promise<string>;
38
+ setStackId: (id: string) => Promise<string>;
37
39
  projectId: () => Promise<ContextValue | undefined>;
38
40
  serverId: () => Promise<ContextValue | undefined>;
39
41
  orgId: () => Promise<ContextValue | undefined>;
40
42
  appInstallationId: () => Promise<ContextValue | undefined>;
43
+ stackId: () => Promise<ContextValue | undefined>;
41
44
  }
@@ -57,11 +57,18 @@ export default class Context {
57
57
  }
58
58
  async setContextValue(key, value) {
59
59
  if (key in contextIDNormalizers) {
60
- value = await contextIDNormalizers[key](this.apiClient, value);
60
+ value = await contextIDNormalizers[key](this.apiClient, value, this);
61
61
  }
62
62
  await this.persist({ [key]: value });
63
63
  return value;
64
64
  }
65
+ async mustGetContextValue(key) {
66
+ const value = await this.getContextValue(key);
67
+ if (!value) {
68
+ throw new InvalidContextError(`Context value for "${key}" is not set.`);
69
+ }
70
+ return value;
71
+ }
65
72
  async getContextValue(key) {
66
73
  const data = await this.contextData;
67
74
  if (key in data) {
@@ -73,8 +80,10 @@ export default class Context {
73
80
  setServerId = (id) => this.setContextValue("server-id", id);
74
81
  setOrgId = (id) => this.setContextValue("org-id", id);
75
82
  setAppInstallationId = (id) => this.setContextValue("installation-id", id);
83
+ setStackId = (id) => this.setContextValue("stack-id", id);
76
84
  projectId = () => this.getContextValue("project-id");
77
85
  serverId = () => this.getContextValue("server-id");
78
86
  orgId = () => this.getContextValue("org-id");
79
87
  appInstallationId = () => this.getContextValue("installation-id");
88
+ stackId = () => this.getContextValue("stack-id");
80
89
  }
@@ -1,7 +1,6 @@
1
- import { Arg, OptionFlag } from "@oclif/core/interfaces";
1
+ import { AlphabetLowercase, Arg, OptionFlag } from "@oclif/core/interfaces";
2
2
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
3
- import { AlphabetLowercase } from "@oclif/core/interfaces";
4
- import { ContextKey, ContextNames } from "./Context.js";
3
+ import Context, { ContextKey, ContextNames } from "./Context.js";
5
4
  import FlagSet from "./FlagSet.js";
6
5
  export type ContextFlags<N extends ContextNames, TID extends string = ContextKey<N>> = {
7
6
  [k in TID]: OptionFlag<string>;
@@ -33,7 +32,7 @@ export type FlagSetOptions = {
33
32
  display: string;
34
33
  };
35
34
  };
36
- export type NormalizeFn = (apiClient: MittwaldAPIV2Client, id: string) => string | Promise<string>;
35
+ export type NormalizeFn = (apiClient: MittwaldAPIV2Client, id: string, ctx: Context) => string | Promise<string>;
37
36
  export declare function makeMissingContextInputError<TName extends ContextNames>(commandType: {
38
37
  flags: {
39
38
  [k in ContextKey<TName>]: OptionFlag<string>;
@@ -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,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>>;