@mittwald/cli 1.5.0 → 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.
@@ -34,23 +34,7 @@ export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
34
34
  * @returns A properly formatted container service request
35
35
  */
36
36
  private buildServiceRequest;
37
- /**
38
- * Parses environment variables from command line flags and env files
39
- *
40
- * @returns An object containing environment variable key-value pairs
41
- */
42
- private parseEnvironmentVariables;
43
- private parseEnvironmentVariablesFromFile;
44
- private parseEnvironmentVariablesFromEnvFlags;
45
- /**
46
- * Determines which ports to expose based on flags and image metadata
47
- *
48
- * @param imageMeta Metadata about the container image
49
- * @returns An array of port mappings
50
- */
51
- private getPortMappings;
52
37
  private getImageAndMeta;
53
- private getImageMeta;
54
38
  private getServiceName;
55
39
  protected render({ serviceId }: Result): ReactNode;
56
40
  }
@@ -7,9 +7,7 @@ import { Success } from "../../rendering/react/components/Success.js";
7
7
  import { Value } from "../../rendering/react/components/Value.js";
8
8
  import * as dockerNames from "docker-names";
9
9
  import { assertStatus } from "@mittwald/api-client";
10
- import * as fs from "fs/promises";
11
- import { parse } from "envfile";
12
- import { pathExists } from "../../lib/util/fs/pathExists.js";
10
+ import { parseEnvironmentVariables, getPortMappings, getImageMeta, } from "../../lib/resources/container/containerconfig.js";
13
11
  export class Run extends ExecRenderBaseCommand {
14
12
  static summary = "Creates and starts a new container.";
15
13
  static flags = {
@@ -133,8 +131,8 @@ export class Run extends ExecRenderBaseCommand {
133
131
  ? [this.flags.entrypoint]
134
132
  : imageMeta.entrypoint;
135
133
  const description = this.flags.description ?? serviceName;
136
- const envs = await this.parseEnvironmentVariables();
137
- const ports = this.getPortMappings(imageMeta);
134
+ const envs = await parseEnvironmentVariables(this.flags.env, this.flags["env-file"]);
135
+ const ports = getPortMappings(imageMeta, this.flags["publish-all"], this.flags.publish);
138
136
  const volumes = this.flags.volume;
139
137
  return {
140
138
  image,
@@ -146,66 +144,11 @@ export class Run extends ExecRenderBaseCommand {
146
144
  volumes,
147
145
  };
148
146
  }
149
- /**
150
- * Parses environment variables from command line flags and env files
151
- *
152
- * @returns An object containing environment variable key-value pairs
153
- */
154
- async parseEnvironmentVariables() {
155
- return {
156
- ...this.parseEnvironmentVariablesFromEnvFlags(),
157
- ...(await this.parseEnvironmentVariablesFromFile()),
158
- };
159
- }
160
- async parseEnvironmentVariablesFromFile() {
161
- const result = {};
162
- for (const envFile of this.flags["env-file"] ?? []) {
163
- if (!(await pathExists(envFile))) {
164
- throw new Error(`Env file not found: ${envFile}`);
165
- }
166
- const fileContent = await fs.readFile(envFile, { encoding: "utf-8" });
167
- const parsed = parse(fileContent);
168
- Object.assign(result, parsed);
169
- }
170
- return result;
171
- }
172
- parseEnvironmentVariablesFromEnvFlags() {
173
- const splitIntoKeyAndValue = (e) => e.split("=", 2);
174
- const envFlags = this.flags.env ?? [];
175
- return Object.fromEntries(envFlags.map(splitIntoKeyAndValue));
176
- }
177
- /**
178
- * Determines which ports to expose based on flags and image metadata
179
- *
180
- * @param imageMeta Metadata about the container image
181
- * @returns An array of port mappings
182
- */
183
- getPortMappings(imageMeta) {
184
- if (this.flags["publish-all"]) {
185
- const definedPorts = imageMeta.exposedPorts ?? [];
186
- const concatPort = (p) => {
187
- const [port, protocol = "tcp"] = p.port.split("/", 2);
188
- return `${port}:${port}/${protocol}`;
189
- };
190
- return definedPorts.map(concatPort);
191
- }
192
- return this.flags.publish ?? [];
193
- }
194
147
  async getImageAndMeta(projectId) {
195
148
  const { image } = this.args;
196
- const meta = await this.getImageMeta(image, projectId);
149
+ const meta = await getImageMeta(this.apiClient, image, projectId);
197
150
  return { image, meta };
198
151
  }
199
- async getImageMeta(image, projectId) {
200
- const resp = await this.apiClient.container.getContainerImageConfig({
201
- queryParameters: {
202
- imageReference: image,
203
- useCredentialsForProjectId: projectId,
204
- },
205
- });
206
- assertStatus(resp, 200);
207
- return resp.data;
208
- }
209
152
  getServiceName() {
210
153
  const { name } = this.flags;
211
154
  if (name !== undefined) {
@@ -0,0 +1,36 @@
1
+ import { ReactNode } from "react";
2
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
3
+ type Result = {
4
+ serviceId: string;
5
+ };
6
+ export declare class Update extends ExecRenderBaseCommand<typeof Update, Result> {
7
+ static summary: string;
8
+ static description: string;
9
+ static flags: {
10
+ image: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ env: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ "env-file": import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ entrypoint: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ command: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ publish: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ "publish-all": import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ volume: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ recreate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
21
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ };
23
+ static args: {
24
+ "container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
25
+ };
26
+ protected exec(): Promise<Result>;
27
+ /**
28
+ * Builds a container service update request from command line flags
29
+ *
30
+ * @returns A properly formatted container service request with only the
31
+ * fields to update
32
+ */
33
+ private buildUpdateRequest;
34
+ protected render({ serviceId }: Result): ReactNode;
35
+ }
36
+ export {};
@@ -0,0 +1,174 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Args, Flags } from "@oclif/core";
3
+ import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
4
+ import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
5
+ import { projectFlags } from "../../lib/resources/project/flags.js";
6
+ import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
7
+ import assertSuccess from "../../lib/apiutil/assert_success.js";
8
+ import { Success } from "../../rendering/react/components/Success.js";
9
+ import { Value } from "../../rendering/react/components/Value.js";
10
+ import { assertStatus } from "@mittwald/api-client";
11
+ import { parseEnvironmentVariables, getPortMappings, getImageMeta, } from "../../lib/resources/container/containerconfig.js";
12
+ export class Update extends ExecRenderBaseCommand {
13
+ static summary = "Updates an existing container.";
14
+ static description = "Updates attributes of an existing container such as image, environment variables, etc.";
15
+ static flags = {
16
+ ...processFlags,
17
+ ...projectFlags,
18
+ image: Flags.string({
19
+ summary: "update the container image",
20
+ description: "Specify a new image to use for the container.",
21
+ required: false,
22
+ }),
23
+ env: Flags.string({
24
+ summary: "set environment variables in the container",
25
+ description: "Format: KEY=VALUE. Multiple environment variables can be specified with multiple --env flags.",
26
+ required: false,
27
+ multiple: true,
28
+ char: "e",
29
+ }),
30
+ "env-file": Flags.string({
31
+ summary: "read environment variables from a file",
32
+ description: "The file should contain lines in the format KEY=VALUE. Multiple files can be specified with multiple --env-file flags.",
33
+ multiple: true,
34
+ required: false,
35
+ }),
36
+ description: Flags.string({
37
+ summary: "update the descriptive label of the container",
38
+ description: "This helps identify the container's purpose or contents.",
39
+ required: false,
40
+ }),
41
+ entrypoint: Flags.string({
42
+ summary: "override the entrypoint of the container",
43
+ description: "The entrypoint is the command that will be executed when the container starts.",
44
+ required: false,
45
+ }),
46
+ command: Flags.string({
47
+ summary: "update the command to run in the container",
48
+ description: "This overrides the default command specified in the container image.",
49
+ required: false,
50
+ }),
51
+ publish: Flags.string({
52
+ summary: "update the container's port mappings",
53
+ description: "Map a container's port to a port on the host system. " +
54
+ "Format: <host-port>:<container-port> or just <container-port> (in which case the host port will be automatically assigned). " +
55
+ "Use multiple -p flags to publish multiple ports.",
56
+ required: false,
57
+ multiple: true,
58
+ char: "p",
59
+ }),
60
+ "publish-all": Flags.boolean({
61
+ summary: "publish all ports that are defined in the image",
62
+ description: "Automatically publish all ports that are exposed by the container image to random ports on the host.",
63
+ required: false,
64
+ char: "P",
65
+ }),
66
+ volume: Flags.string({
67
+ summary: "update volume mounts for the container",
68
+ description: "This flag can be used to add volume mounts to the container. It can be used multiple times to mount multiple volumes." +
69
+ "" +
70
+ "Needs to be in the format <host-path>:<container-path>. " +
71
+ "" +
72
+ "If you specify a file path as volume, this will mount a path from your hosting environment's file system (NOT your local file system) into the container. " +
73
+ "You can also specify a named volume, which needs to be created beforehand.",
74
+ required: false,
75
+ char: "v",
76
+ multiple: true,
77
+ }),
78
+ recreate: Flags.boolean({
79
+ summary: "recreate the container after updating",
80
+ description: "If set, the container will be automatically recreated after updating its configuration.",
81
+ required: false,
82
+ default: false,
83
+ }),
84
+ };
85
+ static args = {
86
+ "container-id": Args.string({
87
+ description: "ID or short ID of the container to update",
88
+ required: true,
89
+ }),
90
+ };
91
+ async exec() {
92
+ const p = makeProcessRenderer(this.flags, "Updating container");
93
+ const [serviceId, stackId] = await withContainerAndStackId(this.apiClient, Update, this.flags, this.args, this.config);
94
+ const service = await p.runStep("getting container configuration", async () => {
95
+ const r = await this.apiClient.container.getService({
96
+ serviceId,
97
+ stackId,
98
+ });
99
+ assertStatus(r, 200);
100
+ return r.data;
101
+ });
102
+ const updatePayload = await p.runStep("preparing update request", this.buildUpdateRequest());
103
+ if (Object.keys(updatePayload).length === 0) {
104
+ await p.complete(_jsx(Success, { children: "Nothing to change. Have a good day!" }));
105
+ return { serviceId };
106
+ }
107
+ await p.runStep("updating container configuration", async () => {
108
+ const r = await this.apiClient.container.updateStack({
109
+ stackId,
110
+ data: {
111
+ services: {
112
+ [service.serviceName]: updatePayload,
113
+ },
114
+ },
115
+ });
116
+ assertStatus(r, 200);
117
+ });
118
+ // Recreate the container if requested
119
+ if (this.flags.recreate) {
120
+ await p.runStep("recreating container", async () => {
121
+ const r = await this.apiClient.container.recreateService({
122
+ serviceId,
123
+ stackId,
124
+ });
125
+ assertSuccess(r);
126
+ });
127
+ }
128
+ await p.complete(_jsxs(Success, { children: ["Container ", _jsx(Value, { children: serviceId }), " was successfully updated.", this.flags.recreate &&
129
+ " The container was recreated with the new configuration."] }));
130
+ return { serviceId };
131
+ }
132
+ /**
133
+ * Builds a container service update request from command line flags
134
+ *
135
+ * @returns A properly formatted container service request with only the
136
+ * fields to update
137
+ */
138
+ async buildUpdateRequest() {
139
+ const updateRequest = {};
140
+ if (this.flags.image) {
141
+ updateRequest.image = this.flags.image;
142
+ // Get image metadata for port mappings if publish-all is specified
143
+ if (this.flags["publish-all"]) {
144
+ const projectId = await this.withProjectId(Update);
145
+ const imageMeta = await getImageMeta(this.apiClient, this.flags.image, projectId);
146
+ updateRequest.ports = getPortMappings(imageMeta, true);
147
+ }
148
+ }
149
+ if (this.flags.command) {
150
+ updateRequest.command = [this.flags.command];
151
+ }
152
+ if (this.flags.entrypoint) {
153
+ updateRequest.entrypoint = [this.flags.entrypoint];
154
+ }
155
+ if (this.flags.description) {
156
+ updateRequest.description = this.flags.description;
157
+ }
158
+ if (this.flags.env || this.flags["env-file"]) {
159
+ updateRequest.envs = await parseEnvironmentVariables(this.flags.env, this.flags["env-file"]);
160
+ }
161
+ if (this.flags.publish) {
162
+ updateRequest.ports = this.flags.publish;
163
+ }
164
+ if (this.flags.volume) {
165
+ updateRequest.volumes = this.flags.volume;
166
+ }
167
+ return updateRequest;
168
+ }
169
+ render({ serviceId }) {
170
+ if (this.flags.quiet) {
171
+ return serviceId;
172
+ }
173
+ }
174
+ }
@@ -3,6 +3,7 @@ import { ReactNode } from "react";
3
3
  import { ProcessRenderer } from "../../../rendering/process/process.js";
4
4
  import { FlagInput, OutputFlags } from "@oclif/core/interfaces";
5
5
  import ByteQuantity from "../../../lib/units/ByteQuantity.js";
6
+ import { CommandFlags } from "../../../lib/basecommands/CommandFlags.js";
6
7
  type CreateResult = {
7
8
  addressId: string;
8
9
  generatedPassword: string | null;
@@ -27,7 +28,7 @@ export default class Create extends ExecRenderBaseCommand<typeof Create, CreateR
27
28
  }[];
28
29
  protected getPassword(process: ProcessRenderer): Promise<[string, boolean]>;
29
30
  protected createForwardAddress(projectId: string, process: ProcessRenderer, flags: OutputFlags<FlagInput<typeof Create.flags>>): Promise<CreateResult>;
30
- protected createMailAddress(projectId: string, process: ProcessRenderer, flags: OutputFlags<FlagInput<typeof Create.flags>>): Promise<CreateResult>;
31
+ protected createMailAddress(projectId: string, process: ProcessRenderer, flags: CommandFlags<typeof Create>): Promise<CreateResult>;
31
32
  protected exec(): Promise<CreateResult>;
32
33
  protected render(executionResult: CreateResult): ReactNode;
33
34
  }
@@ -116,6 +116,7 @@ export default class Create extends ExecRenderBaseCommand {
116
116
  }
117
117
  async createMailAddress(projectId, process, flags) {
118
118
  const [password, passwordGenerated] = await this.getPassword(process);
119
+ const { quota } = flags;
119
120
  const response = await process.runStep("creating mail address", async () => {
120
121
  const response = await this.apiClient.mail.createMailAddress({
121
122
  projectId,
@@ -124,7 +125,7 @@ export default class Create extends ExecRenderBaseCommand {
124
125
  isCatchAll: flags["catch-all"],
125
126
  mailbox: {
126
127
  password,
127
- quotaInBytes: flags.quota * 1024 * 1024,
128
+ quotaInBytes: quota.bytes,
128
129
  enableSpamProtection: flags["enable-spam-protection"],
129
130
  },
130
131
  },
@@ -45,11 +45,14 @@ export declare class ListOwn extends ListBaseCommand<typeof ListOwn, ResponseIte
45
45
  vatId?: string | undefined;
46
46
  vatIdValidationState?: "valid" | "invalid" | "pending" | "unspecified" | undefined;
47
47
  };
48
+ avatarRef?: string;
48
49
  customerId: string;
49
50
  email: string;
50
51
  expiresAt?: string;
52
+ firstName: string;
51
53
  id: string;
52
54
  inviteId?: string;
55
+ lastName: string;
53
56
  memberSince?: string;
54
57
  mfa: boolean;
55
58
  role: MittwaldAPIV2.Components.Schemas.MembershipCustomerRoles;
@@ -48,11 +48,14 @@ export declare class List extends ListBaseCommand<typeof List, ResponseItem, Res
48
48
  registeredAt?: string | undefined;
49
49
  userId: string;
50
50
  };
51
+ avatarRef?: string;
51
52
  customerId: string;
52
53
  email: string;
53
54
  expiresAt?: string;
55
+ firstName: string;
54
56
  id: string;
55
57
  inviteId?: string;
58
+ lastName: string;
56
59
  memberSince?: string;
57
60
  mfa: boolean;
58
61
  role: MittwaldAPIV2.Components.Schemas.MembershipCustomerRoles;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {