@mittwald/cli 1.4.3 → 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.
- package/README.md +3 -0
- package/dist/commands/app/dependency/versions.d.ts +1 -1
- 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 +57 -0
- package/dist/commands/container/run.js +224 -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/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/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/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/dist/rendering/setup/FlagSupportedSetup.d.ts +1 -1
- package/package.json +21 -17
package/README.md
CHANGED
|
@@ -107,6 +107,7 @@ USAGE
|
|
|
107
107
|
* [`mw app`](docs/app.md) - Manage apps, and app installations in your projects
|
|
108
108
|
* [`mw autocomplete`](docs/autocomplete.md) - Display autocomplete installation instructions.
|
|
109
109
|
* [`mw backup`](docs/backup.md) - Manage backups of your projects
|
|
110
|
+
* [`mw container`](docs/container.md) - Delete a container
|
|
110
111
|
* [`mw context`](docs/context.md) - Save certain environment parameters for later use
|
|
111
112
|
* [`mw conversation`](docs/conversation.md) - Manage your support cases
|
|
112
113
|
* [`mw cronjob`](docs/cronjob.md) - Manage cronjobs of your projects
|
|
@@ -119,9 +120,11 @@ USAGE
|
|
|
119
120
|
* [`mw mail`](docs/mail.md) - Manage mailboxes and mail addresses in your projects
|
|
120
121
|
* [`mw org`](docs/org.md) - Manage your organizations, and also any kinds of user memberships concerning these organizations.
|
|
121
122
|
* [`mw project`](docs/project.md) - Manage your projects, and also any kinds of user memberships concerning these projects.
|
|
123
|
+
* [`mw registry`](docs/registry.md) - Manage container registries
|
|
122
124
|
* [`mw server`](docs/server.md) - Manage your servers
|
|
123
125
|
* [`mw sftp-user`](docs/sftp-user.md) - Manage SFTP users of your projects
|
|
124
126
|
* [`mw ssh-user`](docs/ssh-user.md) - Manage SSH users of your projects
|
|
127
|
+
* [`mw stack`](docs/stack.md) - Manage container stacks
|
|
125
128
|
* [`mw update`](docs/update.md) - update the mw CLI
|
|
126
129
|
* [`mw user`](docs/user.md) - Manage your own user account
|
|
127
130
|
|
|
@@ -2,7 +2,7 @@ import { Simplify } from "@mittwald/api-client-commons";
|
|
|
2
2
|
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
3
3
|
import { ListBaseCommand, SorterFunction } from "../../../lib/basecommands/ListBaseCommand.js";
|
|
4
4
|
import { ListColumns } from "../../../rendering/formatter/ListFormatter.js";
|
|
5
|
-
type ResponseItem = Simplify<MittwaldAPIV2.Paths.
|
|
5
|
+
type ResponseItem = Simplify<MittwaldAPIV2.Paths.V2SystemSoftwaresSystemSoftwareIdVersions.Get.Responses.$200.Content.ApplicationJson[number]>;
|
|
6
6
|
type Response = Awaited<ReturnType<MittwaldAPIV2Client["app"]["listSystemsoftwareversions"]>>;
|
|
7
7
|
export declare class Versions extends ListBaseCommand<typeof Versions, ResponseItem, Response> {
|
|
8
8
|
static description: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DeleteBaseCommand } from "../../lib/basecommands/DeleteBaseCommand.js";
|
|
2
|
+
export default class Delete extends DeleteBaseCommand<typeof Delete> {
|
|
3
|
+
static description: string;
|
|
4
|
+
static resourceName: string;
|
|
5
|
+
static aliases: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
8
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
static args: {
|
|
12
|
+
"container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
13
|
+
};
|
|
14
|
+
protected deleteResource(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
3
|
+
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
4
|
+
import assertSuccess from "../../lib/apiutil/assert_success.js";
|
|
5
|
+
import { DeleteBaseCommand } from "../../lib/basecommands/DeleteBaseCommand.js";
|
|
6
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
7
|
+
export default class Delete extends DeleteBaseCommand {
|
|
8
|
+
static description = "Delete a container";
|
|
9
|
+
static resourceName = "container";
|
|
10
|
+
static aliases = ["container:rm"];
|
|
11
|
+
static flags = {
|
|
12
|
+
...DeleteBaseCommand.baseFlags,
|
|
13
|
+
...projectFlags,
|
|
14
|
+
};
|
|
15
|
+
static args = {
|
|
16
|
+
"container-id": Args.string({
|
|
17
|
+
description: "ID or short ID of the container to start",
|
|
18
|
+
required: true,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
async deleteResource() {
|
|
22
|
+
const [serviceId, stackId] = await withContainerAndStackId(this.apiClient, Delete, this.flags, this.args, this.config);
|
|
23
|
+
const stackResponse = await this.apiClient.container.getStack({ stackId });
|
|
24
|
+
assertStatus(stackResponse, 200);
|
|
25
|
+
const service = stackResponse.data.services?.find((s) => s.id === serviceId);
|
|
26
|
+
if (!service) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const response = await this.apiClient.container.updateStack({
|
|
30
|
+
stackId,
|
|
31
|
+
data: {
|
|
32
|
+
services: {
|
|
33
|
+
[service.serviceName]: {},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
assertSuccess(response);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Simplify } from "@mittwald/api-client-commons";
|
|
2
|
+
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
3
|
+
import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
|
|
4
|
+
import { ListColumns } from "../../rendering/formatter/Table.js";
|
|
5
|
+
type ResponseItem = Simplify<MittwaldAPIV2.Paths.V2ProjectsProjectIdServices.Get.Responses.$200.Content.ApplicationJson[number]>;
|
|
6
|
+
export type Response = Awaited<ReturnType<MittwaldAPIV2Client["container"]["listServices"]>>;
|
|
7
|
+
export declare class List extends ListBaseCommand<typeof List, ResponseItem, Response> {
|
|
8
|
+
static description: string;
|
|
9
|
+
static aliases: string[];
|
|
10
|
+
static args: {};
|
|
11
|
+
static flags: {
|
|
12
|
+
"project-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<Response>;
|
|
21
|
+
protected getColumns(data: ResponseItem[]): ListColumns<ResponseItem>;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
|
|
2
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
3
|
+
import { makeDateRendererForFormat } from "../../rendering/textformat/formatDate.js";
|
|
4
|
+
export class List extends ListBaseCommand {
|
|
5
|
+
static description = "List containers belonging to a project.";
|
|
6
|
+
static aliases = ["container:ls"];
|
|
7
|
+
static args = {};
|
|
8
|
+
static flags = {
|
|
9
|
+
...ListBaseCommand.baseFlags,
|
|
10
|
+
...projectFlags,
|
|
11
|
+
};
|
|
12
|
+
async getData() {
|
|
13
|
+
const projectId = await this.withProjectId(List);
|
|
14
|
+
return this.apiClient.container.listServices({ projectId });
|
|
15
|
+
}
|
|
16
|
+
getColumns(data) {
|
|
17
|
+
const { id, shortId } = super.getColumns(data);
|
|
18
|
+
const dateFormatter = makeDateRendererForFormat(this.flags.output, !this.flags["no-relative-dates"]);
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
stackId: {
|
|
22
|
+
header: "Stack ID",
|
|
23
|
+
extended: true,
|
|
24
|
+
exactWidth: 40,
|
|
25
|
+
},
|
|
26
|
+
shortId,
|
|
27
|
+
name: {
|
|
28
|
+
get: (svc) => svc.serviceName,
|
|
29
|
+
},
|
|
30
|
+
image: {
|
|
31
|
+
get: (svc) => svc.deployedState.image,
|
|
32
|
+
},
|
|
33
|
+
command: {
|
|
34
|
+
get: (svc) => svc.deployedState.command?.join(" "),
|
|
35
|
+
},
|
|
36
|
+
description: {},
|
|
37
|
+
ports: {
|
|
38
|
+
get: (svc) => {
|
|
39
|
+
if (svc.deployedState.ports === undefined ||
|
|
40
|
+
svc.deployedState.ports.length === 0) {
|
|
41
|
+
return "no ports";
|
|
42
|
+
}
|
|
43
|
+
return svc.deployedState.ports.join(", ");
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
status: {
|
|
47
|
+
get(svc) {
|
|
48
|
+
return svc.status + " (" + dateFormatter(svc.statusSetAt) + ")";
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from "../../lib/basecommands/BaseCommand.js";
|
|
2
|
+
export declare class Logs extends BaseCommand {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static aliases: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
"no-pager": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
9
|
+
};
|
|
10
|
+
static args: {
|
|
11
|
+
"container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseCommand } from "../../lib/basecommands/BaseCommand.js";
|
|
2
|
+
import { GetBaseCommand } from "../../lib/basecommands/GetBaseCommand.js";
|
|
3
|
+
import { Args, Flags } from "@oclif/core";
|
|
4
|
+
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
5
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
6
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
7
|
+
import { printToPager } from "../../lib/util/pager.js";
|
|
8
|
+
export class Logs extends BaseCommand {
|
|
9
|
+
static summary = "Display logs of a specific container.";
|
|
10
|
+
static description = "This command prints the log output of a specific container. " +
|
|
11
|
+
"" +
|
|
12
|
+
'When this command is run in a terminal, the output is piped through a pager. The pager is determined by your PAGER environment variable, with defaulting to "less". You can disable this behavior with the --no-pager flag.';
|
|
13
|
+
static aliases = ["container:ls"];
|
|
14
|
+
static flags = {
|
|
15
|
+
...GetBaseCommand.baseFlags,
|
|
16
|
+
...projectFlags,
|
|
17
|
+
"no-pager": Flags.boolean({
|
|
18
|
+
description: "Disable pager for output.",
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
static args = {
|
|
22
|
+
"container-id": Args.string({
|
|
23
|
+
description: "ID of the container for which to get logs",
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { flags, args } = await this.parse(Logs);
|
|
29
|
+
const [serviceId, stackId] = await withContainerAndStackId(this.apiClient, Logs, flags, args, this.config);
|
|
30
|
+
const usePager = process.stdin.isTTY && !flags["no-pager"];
|
|
31
|
+
const logsResp = await this.apiClient.container.getServiceLogs({
|
|
32
|
+
stackId,
|
|
33
|
+
serviceId,
|
|
34
|
+
});
|
|
35
|
+
assertStatus(logsResp, 200);
|
|
36
|
+
// This is to work around a bug which causes the response to
|
|
37
|
+
// "getServiceLogs" to contain extra NULL bytes.
|
|
38
|
+
// eslint-disable-next-line no-control-regex
|
|
39
|
+
const logs = logsResp.data.replace(/^\x00*/, "");
|
|
40
|
+
if (usePager) {
|
|
41
|
+
printToPager(logs);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.log(logs);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
3
|
+
type Result = {
|
|
4
|
+
serviceId: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class Recreate extends ExecRenderBaseCommand<typeof Recreate, Result> {
|
|
7
|
+
static summary: string;
|
|
8
|
+
static flags: {
|
|
9
|
+
pull: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
12
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
static args: {
|
|
15
|
+
"container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
protected exec(): Promise<Result>;
|
|
18
|
+
protected render({ serviceId }: Result): ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
export class Recreate extends ExecRenderBaseCommand {
|
|
12
|
+
static summary = "Recreates a container.";
|
|
13
|
+
static flags = {
|
|
14
|
+
...processFlags,
|
|
15
|
+
...projectFlags,
|
|
16
|
+
pull: Flags.boolean({
|
|
17
|
+
description: "pull the container image before recreating the container",
|
|
18
|
+
}),
|
|
19
|
+
force: Flags.boolean({
|
|
20
|
+
description: "also recreate the container when it is already up to date",
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
static args = {
|
|
24
|
+
"container-id": Args.string({
|
|
25
|
+
description: "ID or short ID of the container to restart",
|
|
26
|
+
required: true,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
async exec() {
|
|
30
|
+
const p = makeProcessRenderer(this.flags, "Recreating a container");
|
|
31
|
+
const { pull, force } = this.flags;
|
|
32
|
+
const [serviceId, stackId] = await withContainerAndStackId(this.apiClient, Recreate, this.flags, this.args, this.config);
|
|
33
|
+
if (pull) {
|
|
34
|
+
await p.runStep("pulling image and recreating", async () => {
|
|
35
|
+
const r = await this.apiClient.container.pullImageForService({
|
|
36
|
+
serviceId,
|
|
37
|
+
stackId,
|
|
38
|
+
});
|
|
39
|
+
assertSuccess(r);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const service = await p.runStep("getting container status", async () => {
|
|
44
|
+
const r = await this.apiClient.container.getService({
|
|
45
|
+
serviceId,
|
|
46
|
+
stackId,
|
|
47
|
+
});
|
|
48
|
+
assertStatus(r, 200);
|
|
49
|
+
return r.data;
|
|
50
|
+
});
|
|
51
|
+
if (!service.requiresRecreate && !force) {
|
|
52
|
+
p.addInfo("service is already up to date");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await p.runStep("recreating container", async () => {
|
|
56
|
+
const r = await this.apiClient.container.restartService({
|
|
57
|
+
serviceId,
|
|
58
|
+
stackId,
|
|
59
|
+
});
|
|
60
|
+
assertSuccess(r);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await p.complete(_jsxs(Success, { children: ["Container ", _jsx(Value, { children: serviceId }), " was successfully recreated."] }));
|
|
65
|
+
return { serviceId };
|
|
66
|
+
}
|
|
67
|
+
render({ serviceId }) {
|
|
68
|
+
if (this.flags.quiet) {
|
|
69
|
+
return serviceId;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
3
|
+
type Result = {
|
|
4
|
+
serviceId: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class Restart extends ExecRenderBaseCommand<typeof Restart, Result> {
|
|
7
|
+
static summary: string;
|
|
8
|
+
static flags: {
|
|
9
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
10
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
static args: {
|
|
13
|
+
"container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
protected exec(): Promise<Result>;
|
|
16
|
+
protected render({ serviceId }: Result): ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Args } 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
|
+
export class Restart extends ExecRenderBaseCommand {
|
|
11
|
+
static summary = "Restarts a container.";
|
|
12
|
+
static flags = {
|
|
13
|
+
...processFlags,
|
|
14
|
+
...projectFlags,
|
|
15
|
+
};
|
|
16
|
+
static args = {
|
|
17
|
+
"container-id": Args.string({
|
|
18
|
+
description: "ID or short ID of the container to restart",
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
async exec() {
|
|
23
|
+
const p = makeProcessRenderer(this.flags, "Restarting a container");
|
|
24
|
+
const [serviceId, stackId] = await withContainerAndStackId(this.apiClient, Restart, this.flags, this.args, this.config);
|
|
25
|
+
await p.runStep("restarting container", async () => {
|
|
26
|
+
const r = await this.apiClient.container.restartService({
|
|
27
|
+
serviceId,
|
|
28
|
+
stackId,
|
|
29
|
+
});
|
|
30
|
+
assertSuccess(r);
|
|
31
|
+
});
|
|
32
|
+
await p.complete(_jsxs(Success, { children: ["Container ", _jsx(Value, { children: serviceId }), " was successfully restarted."] }));
|
|
33
|
+
return { serviceId };
|
|
34
|
+
}
|
|
35
|
+
render({ serviceId }) {
|
|
36
|
+
if (this.flags.quiet) {
|
|
37
|
+
return serviceId;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
3
|
+
type Result = {
|
|
4
|
+
serviceId: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
|
|
7
|
+
static summary: string;
|
|
8
|
+
static flags: {
|
|
9
|
+
env: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"env-file": import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
entrypoint: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
publish: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
"publish-all": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
volume: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
18
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
};
|
|
20
|
+
static args: {
|
|
21
|
+
image: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
22
|
+
command: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
23
|
+
args: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
24
|
+
};
|
|
25
|
+
protected exec(): Promise<Result>;
|
|
26
|
+
private addServiceToStack;
|
|
27
|
+
/**
|
|
28
|
+
* Builds a container service request from command line arguments and image
|
|
29
|
+
* metadata
|
|
30
|
+
*
|
|
31
|
+
* @param image The container image to use
|
|
32
|
+
* @param imageMeta Metadata about the container image
|
|
33
|
+
* @param serviceName Name of the service to create
|
|
34
|
+
* @returns A properly formatted container service request
|
|
35
|
+
*/
|
|
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
|
+
private getImageAndMeta;
|
|
53
|
+
private getImageMeta;
|
|
54
|
+
private getServiceName;
|
|
55
|
+
protected render({ serviceId }: Result): ReactNode;
|
|
56
|
+
}
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,224 @@
|
|
|
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 { Success } from "../../rendering/react/components/Success.js";
|
|
7
|
+
import { Value } from "../../rendering/react/components/Value.js";
|
|
8
|
+
import * as dockerNames from "docker-names";
|
|
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";
|
|
13
|
+
export class Run extends ExecRenderBaseCommand {
|
|
14
|
+
static summary = "Creates and starts a new container.";
|
|
15
|
+
static flags = {
|
|
16
|
+
...processFlags,
|
|
17
|
+
...projectFlags,
|
|
18
|
+
env: Flags.string({
|
|
19
|
+
summary: "set environment variables in the container",
|
|
20
|
+
description: "Format: KEY=VALUE. Multiple environment variables can be specified with multiple --env flags.",
|
|
21
|
+
required: false,
|
|
22
|
+
multiple: true,
|
|
23
|
+
char: "e",
|
|
24
|
+
}),
|
|
25
|
+
"env-file": Flags.string({
|
|
26
|
+
summary: "read environment variables from a file",
|
|
27
|
+
description: "The file should contain lines in the format KEY=VALUE. Multiple files can be specified with multiple --env-file flags.",
|
|
28
|
+
multiple: true,
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
description: Flags.string({
|
|
32
|
+
summary: "add a descriptive label to the container",
|
|
33
|
+
description: "This helps identify the container's purpose or contents.",
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
entrypoint: Flags.string({
|
|
37
|
+
summary: "override the default entrypoint of the container image",
|
|
38
|
+
description: "The entrypoint is the command that will be executed when the container starts. If omitted, the entrypoint defined in the image will be used.",
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
name: Flags.string({
|
|
42
|
+
summary: "assign a custom name to the container",
|
|
43
|
+
description: "This makes it easier to reference the container in subsequent commands. If omitted, a random name will be generated automatically.",
|
|
44
|
+
required: false,
|
|
45
|
+
}),
|
|
46
|
+
publish: Flags.string({
|
|
47
|
+
summary: "publish a container's port(s) to the host",
|
|
48
|
+
description: "Map a container's port to a port on the host system. " +
|
|
49
|
+
"Format: <host-port>:<container-port> or just <container-port> (in which case the host port will be automatically assigned). " +
|
|
50
|
+
"For example, -p 8080:80 maps port 80 in the container to port 8080 on the host. " +
|
|
51
|
+
"Use multiple -p flags to publish multiple ports.",
|
|
52
|
+
required: false,
|
|
53
|
+
multiple: true,
|
|
54
|
+
char: "p",
|
|
55
|
+
}),
|
|
56
|
+
"publish-all": Flags.boolean({
|
|
57
|
+
summary: "publish all ports that are defined in the image",
|
|
58
|
+
description: "Automatically publish all ports that are exposed by the container image to random ports on the host.",
|
|
59
|
+
required: false,
|
|
60
|
+
char: "P",
|
|
61
|
+
}),
|
|
62
|
+
volume: Flags.string({
|
|
63
|
+
summary: "bind mount a volume to the container",
|
|
64
|
+
description: "This flag can be used to add volume mounts to the container. It can be used multiple times to mount multiple volumes." +
|
|
65
|
+
"" +
|
|
66
|
+
"Needs to be in the format <host-path>:<container-path>. " +
|
|
67
|
+
"" +
|
|
68
|
+
"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. " +
|
|
69
|
+
"You can also specify a named volume, which needs to be created beforehand.",
|
|
70
|
+
required: false,
|
|
71
|
+
char: "v",
|
|
72
|
+
multiple: true,
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
static args = {
|
|
76
|
+
image: Args.string({
|
|
77
|
+
summary: "container image to run",
|
|
78
|
+
description: "Can be specified as a repository/tag or repository@digest (e.g., 'ubuntu:20.04' or 'alpine@sha256:abc123...'). If no tag is provided, 'latest' is assumed.",
|
|
79
|
+
required: true,
|
|
80
|
+
}),
|
|
81
|
+
command: Args.string({
|
|
82
|
+
summary: "command to run in the container",
|
|
83
|
+
description: "This overrides the default command specified in the container image. If omitted, the default command from the image will be used. For example, 'bash' or 'python app.py'.",
|
|
84
|
+
required: false,
|
|
85
|
+
}),
|
|
86
|
+
args: Args.string({
|
|
87
|
+
summary: "arguments to pass to the command",
|
|
88
|
+
description: "These are the runtime arguments passed to the command specified by the command parameter or the container's default command, not to the container itself. For example, if the command is 'echo', the args might be 'hello world'.",
|
|
89
|
+
required: false,
|
|
90
|
+
variadic: true,
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
async exec() {
|
|
94
|
+
const p = makeProcessRenderer(this.flags, "Creating a container");
|
|
95
|
+
const projectId = await this.withProjectId(Run);
|
|
96
|
+
const stackId = projectId;
|
|
97
|
+
const serviceName = this.getServiceName();
|
|
98
|
+
const { image, meta: imageMeta } = await p.runStep("getting image metadata", this.getImageAndMeta(projectId));
|
|
99
|
+
const serviceRequest = await p.runStep("preparing service request", this.buildServiceRequest(image, imageMeta, serviceName));
|
|
100
|
+
const stack = await p.runStep("creating container", this.addServiceToStack(stackId, serviceName, serviceRequest));
|
|
101
|
+
const service = stack.services?.find(matchServiceByName(serviceName));
|
|
102
|
+
const serviceId = service?.id;
|
|
103
|
+
if (!serviceId) {
|
|
104
|
+
throw new Error("Service ID not found in the created stack.");
|
|
105
|
+
}
|
|
106
|
+
await p.complete(_jsxs(Success, { children: ["Container ", _jsx(Value, { children: serviceId }), " was successfully created and started."] }));
|
|
107
|
+
return { serviceId };
|
|
108
|
+
}
|
|
109
|
+
async addServiceToStack(stackId, serviceName, serviceRequest) {
|
|
110
|
+
const resp = await this.apiClient.container.updateStack({
|
|
111
|
+
stackId,
|
|
112
|
+
data: {
|
|
113
|
+
services: {
|
|
114
|
+
[serviceName]: serviceRequest,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
assertStatus(resp, 200);
|
|
119
|
+
return resp.data;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds a container service request from command line arguments and image
|
|
123
|
+
* metadata
|
|
124
|
+
*
|
|
125
|
+
* @param image The container image to use
|
|
126
|
+
* @param imageMeta Metadata about the container image
|
|
127
|
+
* @param serviceName Name of the service to create
|
|
128
|
+
* @returns A properly formatted container service request
|
|
129
|
+
*/
|
|
130
|
+
async buildServiceRequest(image, imageMeta, serviceName) {
|
|
131
|
+
const command = this.args.command ? [this.args.command] : imageMeta.command;
|
|
132
|
+
const entrypoint = this.flags.entrypoint
|
|
133
|
+
? [this.flags.entrypoint]
|
|
134
|
+
: imageMeta.entrypoint;
|
|
135
|
+
const description = this.flags.description ?? serviceName;
|
|
136
|
+
const envs = await this.parseEnvironmentVariables();
|
|
137
|
+
const ports = this.getPortMappings(imageMeta);
|
|
138
|
+
const volumes = this.flags.volume;
|
|
139
|
+
return {
|
|
140
|
+
image,
|
|
141
|
+
command,
|
|
142
|
+
entrypoint,
|
|
143
|
+
description,
|
|
144
|
+
envs,
|
|
145
|
+
ports,
|
|
146
|
+
volumes,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
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
|
+
async getImageAndMeta(projectId) {
|
|
195
|
+
const { image } = this.args;
|
|
196
|
+
const meta = await this.getImageMeta(image, projectId);
|
|
197
|
+
return { image, meta };
|
|
198
|
+
}
|
|
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
|
+
getServiceName() {
|
|
210
|
+
const { name } = this.flags;
|
|
211
|
+
if (name !== undefined) {
|
|
212
|
+
return name;
|
|
213
|
+
}
|
|
214
|
+
return dockerNames.getRandomName();
|
|
215
|
+
}
|
|
216
|
+
render({ serviceId }) {
|
|
217
|
+
if (this.flags.quiet) {
|
|
218
|
+
return serviceId;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function matchServiceByName(name) {
|
|
223
|
+
return (service) => service.serviceName === name;
|
|
224
|
+
}
|