@mittwald/cli 1.6.0 → 1.8.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 +1 -1
- package/dist/commands/app/upgrade.d.ts +14 -0
- package/dist/commands/app/upgrade.js +35 -22
- package/dist/commands/container/cp.d.ts +23 -0
- package/dist/commands/container/cp.js +130 -0
- package/dist/commands/container/exec.d.ts +25 -0
- package/dist/commands/container/exec.js +93 -0
- package/dist/commands/container/port-forward.d.ts +20 -0
- package/dist/commands/container/port-forward.js +68 -0
- package/dist/commands/container/run.d.ts +12 -0
- package/dist/commands/container/run.js +27 -8
- package/dist/commands/container/ssh.d.ts +17 -0
- package/dist/commands/container/ssh.js +61 -0
- package/dist/commands/domain/dnszone/update.js +3 -2
- package/dist/lib/basecommands/BaseCommand.js +2 -1
- package/dist/lib/error/NoTokenFoundError.d.ts +11 -0
- package/dist/lib/error/NoTokenFoundError.js +13 -0
- package/dist/lib/resources/ssh/container.d.ts +3 -0
- package/dist/lib/resources/ssh/container.js +29 -0
- package/dist/lib/units/PortMapping.d.ts +11 -0
- package/dist/lib/units/PortMapping.js +38 -0
- package/dist/lib/units/PortMapping.test.d.ts +1 -0
- package/dist/lib/units/PortMapping.test.js +28 -0
- package/dist/rendering/react/components/ErrorBox.js +4 -0
- package/package.json +62 -14
package/README.md
CHANGED
|
@@ -107,7 +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) -
|
|
110
|
+
* [`mw container`](docs/container.md) - Manage containers
|
|
111
111
|
* [`mw context`](docs/context.md) - Save certain environment parameters for later use
|
|
112
112
|
* [`mw conversation`](docs/conversation.md) - Manage your support cases
|
|
113
113
|
* [`mw cronjob`](docs/cronjob.md) - Manage cronjobs of your projects
|
|
@@ -14,5 +14,19 @@ export declare class UpgradeApp extends ExecRenderBaseCommand<typeof UpgradeApp,
|
|
|
14
14
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
protected exec(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Determines the target application version based on the provided input and
|
|
19
|
+
* available upgrade candidates.
|
|
20
|
+
*
|
|
21
|
+
* @param currentApp The current application instance.
|
|
22
|
+
* @param currentAppVersion The current version of the application.
|
|
23
|
+
* @param targetAppVersionCandidates List of potential target application
|
|
24
|
+
* versions.
|
|
25
|
+
* @param process The process renderer to handle user interactions and display
|
|
26
|
+
* information.
|
|
27
|
+
* @returns The determined target application version, or undefined if not
|
|
28
|
+
* resolved.
|
|
29
|
+
*/
|
|
30
|
+
private determineTargetAppVersion;
|
|
17
31
|
protected render(): ReactNode;
|
|
18
32
|
}
|
|
@@ -11,7 +11,7 @@ import { Success } from "../../rendering/react/components/Success.js";
|
|
|
11
11
|
import { waitUntilAppStateHasNormalized } from "../../lib/resources/app/wait.js";
|
|
12
12
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
13
13
|
import { waitFlags } from "../../lib/wait.js";
|
|
14
|
-
import semver from "semver
|
|
14
|
+
import semver from "semver";
|
|
15
15
|
export class UpgradeApp extends ExecRenderBaseCommand {
|
|
16
16
|
static description = "Upgrade app installation to target version";
|
|
17
17
|
static args = {
|
|
@@ -19,11 +19,11 @@ export class UpgradeApp extends ExecRenderBaseCommand {
|
|
|
19
19
|
};
|
|
20
20
|
static flags = {
|
|
21
21
|
"target-version": Flags.string({
|
|
22
|
-
description: "target version to upgrade app to; if omitted, target version will be prompted interactively",
|
|
22
|
+
description: "target version to upgrade app to; if omitted, target version will be prompted interactively. May also be a semantic versioning range, e.g. ^1.0.0. If set to 'latest', the latest available version will be used.",
|
|
23
23
|
}),
|
|
24
24
|
force: Flags.boolean({
|
|
25
25
|
char: "f",
|
|
26
|
-
|
|
26
|
+
summary: "do not ask for confirmation.",
|
|
27
27
|
}),
|
|
28
28
|
...projectFlags,
|
|
29
29
|
...processFlags,
|
|
@@ -43,25 +43,7 @@ export class UpgradeApp extends ExecRenderBaseCommand {
|
|
|
43
43
|
process.complete(_jsxs(Text, { children: ["Your ", currentApp.name, " ", currentAppVersion.externalVersion, " is already Up-To-Date. \u2705"] }));
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
if (this.flags["target-version"] == "latest") {
|
|
48
|
-
targetAppVersion =
|
|
49
|
-
(await getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates(this.apiClient, currentApp.id, currentAppVersion.id));
|
|
50
|
-
}
|
|
51
|
-
else if (this.flags["target-version"]) {
|
|
52
|
-
const targetVersionMatchFromCandidates = targetAppVersionCandidates.find((targetAppVersionCandidate) => targetAppVersionCandidate.externalVersion ===
|
|
53
|
-
this.flags["target-version"]);
|
|
54
|
-
if (targetVersionMatchFromCandidates) {
|
|
55
|
-
targetAppVersion = targetVersionMatchFromCandidates;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
process.addInfo(_jsx(Text, { children: "The given target upgrade version does not seem to be a valid upgrade candidate." }));
|
|
59
|
-
targetAppVersion = (await forceTargetVersionSelection(process, this.apiClient, targetAppVersionCandidates, currentApp, currentAppVersion));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
targetAppVersion = (await forceTargetVersionSelection(process, this.apiClient, targetAppVersionCandidates, currentApp, currentAppVersion));
|
|
64
|
-
}
|
|
46
|
+
const targetAppVersion = await this.determineTargetAppVersion(currentApp, currentAppVersion, targetAppVersionCandidates, process);
|
|
65
47
|
if (!targetAppVersion) {
|
|
66
48
|
process.error("Target app version could not be determined properly.");
|
|
67
49
|
ux.exit(1);
|
|
@@ -121,6 +103,37 @@ export class UpgradeApp extends ExecRenderBaseCommand {
|
|
|
121
103
|
}
|
|
122
104
|
await process.complete(_jsx(Success, { children: successText }));
|
|
123
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Determines the target application version based on the provided input and
|
|
108
|
+
* available upgrade candidates.
|
|
109
|
+
*
|
|
110
|
+
* @param currentApp The current application instance.
|
|
111
|
+
* @param currentAppVersion The current version of the application.
|
|
112
|
+
* @param targetAppVersionCandidates List of potential target application
|
|
113
|
+
* versions.
|
|
114
|
+
* @param process The process renderer to handle user interactions and display
|
|
115
|
+
* information.
|
|
116
|
+
* @returns The determined target application version, or undefined if not
|
|
117
|
+
* resolved.
|
|
118
|
+
*/
|
|
119
|
+
async determineTargetAppVersion(currentApp, currentAppVersion, targetAppVersionCandidates, process) {
|
|
120
|
+
const targetAppVersionString = this.flags["target-version"];
|
|
121
|
+
if (targetAppVersionString == "latest") {
|
|
122
|
+
return await getLatestAvailableTargetAppVersionForAppVersionUpgradeCandidates(this.apiClient, currentApp.id, currentAppVersion.id);
|
|
123
|
+
}
|
|
124
|
+
if (targetAppVersionString) {
|
|
125
|
+
const exactVersionMatch = targetAppVersionCandidates.find((v) => v.externalVersion === targetAppVersionString);
|
|
126
|
+
if (exactVersionMatch) {
|
|
127
|
+
return exactVersionMatch;
|
|
128
|
+
}
|
|
129
|
+
const semverMatch = targetAppVersionCandidates.findLast((v) => semver.satisfies(v.externalVersion, targetAppVersionString));
|
|
130
|
+
if (semverMatch) {
|
|
131
|
+
return semverMatch;
|
|
132
|
+
}
|
|
133
|
+
process.addInfo(_jsx(Text, { children: "The given target upgrade version does not seem to be a valid upgrade candidate." }));
|
|
134
|
+
}
|
|
135
|
+
return await forceTargetVersionSelection(process, this.apiClient, targetAppVersionCandidates, currentApp, currentAppVersion);
|
|
136
|
+
}
|
|
124
137
|
render() {
|
|
125
138
|
return true;
|
|
126
139
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
export default class Cp extends ExecRenderBaseCommand<typeof Cp, void> {
|
|
4
|
+
static summary: string;
|
|
5
|
+
static description: string;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static args: {
|
|
8
|
+
source: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
9
|
+
dest: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static flags: {
|
|
12
|
+
archive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
recursive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
15
|
+
"ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
};
|
|
19
|
+
private parseContainerPath;
|
|
20
|
+
private buildScpCommand;
|
|
21
|
+
protected exec(): Promise<void>;
|
|
22
|
+
protected render(): ReactNode;
|
|
23
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { Args, Flags } from "@oclif/core";
|
|
4
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
5
|
+
import { getSSHConnectionForContainer } from "../../lib/resources/ssh/container.js";
|
|
6
|
+
import { sshConnectionFlags } from "../../lib/resources/ssh/flags.js";
|
|
7
|
+
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
8
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
9
|
+
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
10
|
+
import { spawnInProcess } from "../../rendering/process/process_exec.js";
|
|
11
|
+
import { Success } from "../../rendering/react/components/Success.js";
|
|
12
|
+
export default class Cp extends ExecRenderBaseCommand {
|
|
13
|
+
static summary = "Copy files/folders between a container and the local filesystem";
|
|
14
|
+
static description = `The syntax is similar to docker cp:
|
|
15
|
+
- Copy from container to host: mw container cp CONTAINER:SRC_PATH DEST_PATH
|
|
16
|
+
- Copy from host to container: mw container cp SRC_PATH CONTAINER:DEST_PATH
|
|
17
|
+
|
|
18
|
+
Where CONTAINER can be a container ID, short ID, or service name.`;
|
|
19
|
+
static examples = [
|
|
20
|
+
"# Copy a file from container to current directory\n<%= config.bin %> <%= command.id %> mycontainer:/app/config.json .",
|
|
21
|
+
"# Copy a file from host to container\n<%= config.bin %> <%= command.id %> ./local-file.txt mycontainer:/app/",
|
|
22
|
+
"# Copy a directory recursively\n<%= config.bin %> <%= command.id %> mycontainer:/var/log ./logs",
|
|
23
|
+
"# Copy with archive mode (preserve permissions)\n<%= config.bin %> <%= command.id %> -a mycontainer:/app/data ./backup",
|
|
24
|
+
];
|
|
25
|
+
static args = {
|
|
26
|
+
source: Args.string({
|
|
27
|
+
description: "Source path (either local path or CONTAINER:PATH)",
|
|
28
|
+
required: true,
|
|
29
|
+
}),
|
|
30
|
+
dest: Args.string({
|
|
31
|
+
description: "Destination path (either local path or CONTAINER:PATH)",
|
|
32
|
+
required: true,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
static flags = {
|
|
36
|
+
...processFlags,
|
|
37
|
+
...sshConnectionFlags,
|
|
38
|
+
...projectFlags,
|
|
39
|
+
archive: Flags.boolean({
|
|
40
|
+
char: "a",
|
|
41
|
+
summary: "archive mode (copy all uid/gid information)",
|
|
42
|
+
description: "Preserve file permissions and ownership when copying",
|
|
43
|
+
}),
|
|
44
|
+
recursive: Flags.boolean({
|
|
45
|
+
char: "r",
|
|
46
|
+
summary: "copy directories recursively",
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
parseContainerPath(path) {
|
|
50
|
+
const colonIndex = path.indexOf(":");
|
|
51
|
+
if (colonIndex === -1) {
|
|
52
|
+
return { path };
|
|
53
|
+
}
|
|
54
|
+
const container = path.substring(0, colonIndex);
|
|
55
|
+
const containerPath = path.substring(colonIndex + 1);
|
|
56
|
+
// Ensure container path starts with / (absolute path)
|
|
57
|
+
const normalizedPath = containerPath.startsWith("/")
|
|
58
|
+
? containerPath
|
|
59
|
+
: `/${containerPath}`;
|
|
60
|
+
return { container, path: normalizedPath };
|
|
61
|
+
}
|
|
62
|
+
buildScpCommand(source, dest, flags) {
|
|
63
|
+
const scpArgs = ["-o", "PasswordAuthentication=no"];
|
|
64
|
+
if (flags.archive) {
|
|
65
|
+
scpArgs.push("-p");
|
|
66
|
+
}
|
|
67
|
+
if (flags.recursive) {
|
|
68
|
+
scpArgs.push("-r");
|
|
69
|
+
}
|
|
70
|
+
scpArgs.push(source, dest);
|
|
71
|
+
return scpArgs;
|
|
72
|
+
}
|
|
73
|
+
async exec() {
|
|
74
|
+
const { args, flags } = await this.parse(Cp);
|
|
75
|
+
const sourceParsed = this.parseContainerPath(args.source);
|
|
76
|
+
const destParsed = this.parseContainerPath(args.dest);
|
|
77
|
+
// Validate that exactly one of source or dest is a container path
|
|
78
|
+
if (sourceParsed.container && destParsed.container) {
|
|
79
|
+
this.error("Cannot copy from container to container. One path must be local.");
|
|
80
|
+
}
|
|
81
|
+
if (!sourceParsed.container && !destParsed.container) {
|
|
82
|
+
this.error("At least one path must specify a container (CONTAINER:PATH format).");
|
|
83
|
+
}
|
|
84
|
+
const containerName = sourceParsed.container || destParsed.container;
|
|
85
|
+
const isDownload = !!sourceParsed.container;
|
|
86
|
+
const p = makeProcessRenderer(flags, `Copying files ${isDownload ? "from" : "to"} container`);
|
|
87
|
+
// Get container connection info
|
|
88
|
+
const [containerId, stackId] = await p.runStep("getting container connection info", async () => {
|
|
89
|
+
return withContainerAndStackId(this.apiClient, Cp, flags, { "container-id": containerName }, this.config);
|
|
90
|
+
});
|
|
91
|
+
const { host, user } = await p.runStep("establishing SSH connection", async () => {
|
|
92
|
+
return getSSHConnectionForContainer(this.apiClient, containerId, stackId, flags["ssh-user"]);
|
|
93
|
+
});
|
|
94
|
+
// Construct source and destination for SCP
|
|
95
|
+
let scpSource;
|
|
96
|
+
let scpDest;
|
|
97
|
+
if (isDownload) {
|
|
98
|
+
scpSource = `${user}@${host}:${sourceParsed.path}`;
|
|
99
|
+
scpDest = destParsed.path;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
scpSource = sourceParsed.path;
|
|
103
|
+
scpDest = `${user}@${host}:${destParsed.path}`;
|
|
104
|
+
}
|
|
105
|
+
// Automatically enable recursive for directories
|
|
106
|
+
const effectiveFlags = { ...flags };
|
|
107
|
+
if (!flags.recursive && !isDownload) {
|
|
108
|
+
try {
|
|
109
|
+
if (fs.existsSync(scpSource)) {
|
|
110
|
+
const stats = fs.statSync(scpSource);
|
|
111
|
+
if (stats.isDirectory()) {
|
|
112
|
+
effectiveFlags.recursive = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (ignored) {
|
|
117
|
+
// Ignore errors when checking if source is a directory
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const scpCommand = this.buildScpCommand(scpSource, scpDest, effectiveFlags);
|
|
121
|
+
await spawnInProcess(p, `copying files ${isDownload ? "from" : "to"} container`, "/usr/bin/scp", scpCommand);
|
|
122
|
+
await p.complete(_jsx(CopySuccess, { isDownload: isDownload }));
|
|
123
|
+
}
|
|
124
|
+
render() {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function CopySuccess({ isDownload }) {
|
|
129
|
+
return (_jsxs(Success, { children: ["Files successfully ", isDownload ? "downloaded from" : "uploaded to", " ", "container! \uD83D\uDE80"] }));
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.js";
|
|
2
|
+
export default class Exec extends ExtendedBaseCommand<typeof Exec> {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static args: {
|
|
6
|
+
"container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
command: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
static flags: {
|
|
10
|
+
workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
env: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
shell: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
14
|
+
"ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Prepare environment variables for the SSH command
|
|
19
|
+
*
|
|
20
|
+
* @param envVars Array of environment variables in KEY=VALUE format
|
|
21
|
+
* @returns Formatted string with export commands
|
|
22
|
+
*/
|
|
23
|
+
private prepareEnvironmentVariables;
|
|
24
|
+
run(): Promise<void>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as child_process from "child_process";
|
|
2
|
+
import { Args, Flags } from "@oclif/core";
|
|
3
|
+
import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.js";
|
|
4
|
+
import { getSSHConnectionForContainer } from "../../lib/resources/ssh/container.js";
|
|
5
|
+
import { sshConnectionFlags } from "../../lib/resources/ssh/flags.js";
|
|
6
|
+
import { sshUsageDocumentation } from "../../lib/resources/ssh/doc.js";
|
|
7
|
+
import { buildSSHClientFlags } from "../../lib/resources/ssh/connection.js";
|
|
8
|
+
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
9
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
10
|
+
import shellEscape from "shell-escape";
|
|
11
|
+
export default class Exec extends ExtendedBaseCommand {
|
|
12
|
+
static summary = "Execute a command in a container via SSH non-interactively.";
|
|
13
|
+
static description = sshUsageDocumentation;
|
|
14
|
+
static args = {
|
|
15
|
+
"container-id": Args.string({
|
|
16
|
+
description: "ID or short ID of the container to connect to",
|
|
17
|
+
required: true,
|
|
18
|
+
}),
|
|
19
|
+
command: Args.string({
|
|
20
|
+
description: "Command to execute in the container",
|
|
21
|
+
required: true,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
static flags = {
|
|
25
|
+
...sshConnectionFlags,
|
|
26
|
+
...projectFlags,
|
|
27
|
+
workdir: Flags.string({
|
|
28
|
+
char: "w",
|
|
29
|
+
summary: "working directory where the command will be executed",
|
|
30
|
+
default: undefined,
|
|
31
|
+
}),
|
|
32
|
+
env: Flags.string({
|
|
33
|
+
char: "e",
|
|
34
|
+
summary: "environment variables to set for the command (format: KEY=VALUE)",
|
|
35
|
+
multiple: true,
|
|
36
|
+
}),
|
|
37
|
+
shell: Flags.string({
|
|
38
|
+
summary: "shell to use for the SSH connection",
|
|
39
|
+
default: "/bin/sh",
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Prepare environment variables for the SSH command
|
|
44
|
+
*
|
|
45
|
+
* @param envVars Array of environment variables in KEY=VALUE format
|
|
46
|
+
* @returns Formatted string with export commands
|
|
47
|
+
*/
|
|
48
|
+
prepareEnvironmentVariables(envVars) {
|
|
49
|
+
return (envVars
|
|
50
|
+
.map((env) => {
|
|
51
|
+
const eqIdx = env.indexOf("=");
|
|
52
|
+
if (eqIdx === -1) {
|
|
53
|
+
// If no '=', treat the whole string as key with empty value
|
|
54
|
+
return `export ${shellEscape([env])}=`;
|
|
55
|
+
}
|
|
56
|
+
const key = env.slice(0, eqIdx);
|
|
57
|
+
const value = env.slice(eqIdx + 1);
|
|
58
|
+
return `export ${shellEscape([key])}=${shellEscape([value])}`;
|
|
59
|
+
})
|
|
60
|
+
.join("; ") + "; ");
|
|
61
|
+
}
|
|
62
|
+
async run() {
|
|
63
|
+
const { args, flags } = await this.parse(Exec);
|
|
64
|
+
const [containerId, stackId] = await withContainerAndStackId(this.apiClient, Exec, flags, this.args, this.config);
|
|
65
|
+
const { host, user } = await getSSHConnectionForContainer(this.apiClient, containerId, stackId, flags["ssh-user"]);
|
|
66
|
+
this.log("executing command on %s as %s", host, user);
|
|
67
|
+
const command = args.command;
|
|
68
|
+
const workdir = flags.workdir;
|
|
69
|
+
// Build the command to execute
|
|
70
|
+
let execCommand = "";
|
|
71
|
+
// Add environment variables if provided
|
|
72
|
+
if (flags.env && flags.env.length > 0) {
|
|
73
|
+
execCommand += this.prepareEnvironmentVariables(flags.env);
|
|
74
|
+
}
|
|
75
|
+
// Change to working directory if specified
|
|
76
|
+
if (workdir !== undefined) {
|
|
77
|
+
execCommand += `cd ${shellEscape([workdir])} && `;
|
|
78
|
+
}
|
|
79
|
+
// Add the actual command
|
|
80
|
+
execCommand += command;
|
|
81
|
+
const sshArgs = buildSSHClientFlags(user, host, flags, {
|
|
82
|
+
interactive: false,
|
|
83
|
+
});
|
|
84
|
+
const wrappedExecCommand = shellEscape([flags.shell, "-c", execCommand]);
|
|
85
|
+
this.debug("running ssh %o, with command %o", sshArgs, wrappedExecCommand);
|
|
86
|
+
const result = child_process.spawnSync("/usr/bin/ssh", [...sshArgs, wrappedExecCommand], {
|
|
87
|
+
stdio: "inherit",
|
|
88
|
+
});
|
|
89
|
+
if (result.status !== 0) {
|
|
90
|
+
this.error(`Command failed with exit code ${result.status}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import PortMapping from "../../lib/units/PortMapping.js";
|
|
4
|
+
export declare class PortForward extends ExecRenderBaseCommand<typeof PortForward, Record<string, never>> {
|
|
5
|
+
static summary: string;
|
|
6
|
+
static description: string;
|
|
7
|
+
static flags: {
|
|
8
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
9
|
+
"ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
static args: {
|
|
14
|
+
"container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
15
|
+
port: import("@oclif/core/interfaces").Arg<PortMapping | undefined, Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
protected exec(): Promise<Record<string, never>>;
|
|
18
|
+
private getPortMappings;
|
|
19
|
+
protected render(): ReactNode;
|
|
20
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
3
|
+
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
4
|
+
import * as cp from "child_process";
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import { Value } from "../../rendering/react/components/Value.js";
|
|
7
|
+
import { Args } from "@oclif/core";
|
|
8
|
+
import { sshConnectionFlags } from "../../lib/resources/ssh/flags.js";
|
|
9
|
+
import { sshUsageDocumentation } from "../../lib/resources/ssh/doc.js";
|
|
10
|
+
import { buildSSHClientFlags } from "../../lib/resources/ssh/connection.js";
|
|
11
|
+
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
12
|
+
import { getSSHConnectionForContainer } from "../../lib/resources/ssh/container.js";
|
|
13
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
14
|
+
import PortMapping from "../../lib/units/PortMapping.js";
|
|
15
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
16
|
+
export class PortForward extends ExecRenderBaseCommand {
|
|
17
|
+
static summary = "Forward a container port to a local port";
|
|
18
|
+
static description = "This command forwards a TCP port from a container to a local port on your machine. This allows you to connect to services running in the container as if they were running on your local machine.\n\n" +
|
|
19
|
+
sshUsageDocumentation;
|
|
20
|
+
static flags = {
|
|
21
|
+
...processFlags,
|
|
22
|
+
...sshConnectionFlags,
|
|
23
|
+
...projectFlags,
|
|
24
|
+
};
|
|
25
|
+
static args = {
|
|
26
|
+
"container-id": Args.string({
|
|
27
|
+
description: "ID or short ID of the container to connect to",
|
|
28
|
+
required: true,
|
|
29
|
+
}),
|
|
30
|
+
port: PortMapping.arg({
|
|
31
|
+
summary: "Port mapping in the format 'local-port:container-port'",
|
|
32
|
+
description: "Specifies the port mapping between your local machine and the container. Format: 'local-port:container-port'. If not specified, available ports will be detected automatically.",
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
async exec() {
|
|
37
|
+
const [serviceId, stackId] = await withContainerAndStackId(this.apiClient, PortForward, this.flags, this.args, this.config);
|
|
38
|
+
const p = makeProcessRenderer(this.flags, "Port-forwarding a container");
|
|
39
|
+
const { host, user } = await getSSHConnectionForContainer(this.apiClient, serviceId, stackId, this.flags["ssh-user"]);
|
|
40
|
+
const portMappings = await this.getPortMappings(stackId, serviceId);
|
|
41
|
+
await p.complete(_jsxs(Box, { flexDirection: "column", children: [portMappings.map((p, idx) => (_jsxs(Text, { children: ["Forwarding container port ", _jsx(Value, { children: p.remotePort }), " to local port ", _jsx(Value, { children: p.localPort }), "."] }, idx))), _jsx(Text, { children: "Use CTRL+C to cancel." })] }));
|
|
42
|
+
const sshArgs = buildSSHClientFlags(user, host, this.flags, {
|
|
43
|
+
interactive: false,
|
|
44
|
+
additionalFlags: portMappings
|
|
45
|
+
.map((p) => ["-L", `${p.localPort}:localhost:${p.remotePort}`])
|
|
46
|
+
.flat(),
|
|
47
|
+
});
|
|
48
|
+
cp.spawnSync("ssh", [...sshArgs, "cat", "/dev/zero"], {
|
|
49
|
+
stdio: ["ignore", process.stdout, process.stderr],
|
|
50
|
+
});
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
async getPortMappings(stackId, serviceId) {
|
|
54
|
+
if (this.args.port) {
|
|
55
|
+
return [this.args.port];
|
|
56
|
+
}
|
|
57
|
+
const containerResponse = await this.apiClient.container.getService({
|
|
58
|
+
stackId,
|
|
59
|
+
serviceId,
|
|
60
|
+
});
|
|
61
|
+
assertStatus(containerResponse, 200);
|
|
62
|
+
const ports = containerResponse.data.deployedState.ports ?? [];
|
|
63
|
+
return ports.map((p) => PortMapping.fromPortAndProtocol(p));
|
|
64
|
+
}
|
|
65
|
+
render() {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -5,6 +5,8 @@ type Result = {
|
|
|
5
5
|
};
|
|
6
6
|
export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
|
|
7
7
|
static summary: string;
|
|
8
|
+
static strict: boolean;
|
|
9
|
+
static usage: string;
|
|
8
10
|
static flags: {
|
|
9
11
|
env: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
"env-file": import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -24,6 +26,16 @@ export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
|
|
|
24
26
|
};
|
|
25
27
|
protected exec(): Promise<Result>;
|
|
26
28
|
private addServiceToStack;
|
|
29
|
+
/**
|
|
30
|
+
* Builds and returns the container command based on the provided image
|
|
31
|
+
* metadata and arguments.
|
|
32
|
+
*
|
|
33
|
+
* @param imageMeta The configuration object containing the metadata of the
|
|
34
|
+
* container image, including the default command.
|
|
35
|
+
* @returns An array of strings representing the container command to execute,
|
|
36
|
+
* or undefined if no specific command is set.
|
|
37
|
+
*/
|
|
38
|
+
private buildContainerCommand;
|
|
27
39
|
/**
|
|
28
40
|
* Builds a container service request from command line arguments and image
|
|
29
41
|
* metadata
|
|
@@ -3,13 +3,16 @@ import { Args, Flags } from "@oclif/core";
|
|
|
3
3
|
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
4
4
|
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
5
5
|
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
6
|
+
import dockerNames from "docker-names";
|
|
7
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
8
|
+
import { getImageMeta, getPortMappings, parseEnvironmentVariables, } from "../../lib/resources/container/containerconfig.js";
|
|
6
9
|
import { Success } from "../../rendering/react/components/Success.js";
|
|
7
10
|
import { Value } from "../../rendering/react/components/Value.js";
|
|
8
|
-
import * as dockerNames from "docker-names";
|
|
9
|
-
import { assertStatus } from "@mittwald/api-client";
|
|
10
|
-
import { parseEnvironmentVariables, getPortMappings, getImageMeta, } from "../../lib/resources/container/containerconfig.js";
|
|
11
11
|
export class Run extends ExecRenderBaseCommand {
|
|
12
12
|
static summary = "Creates and starts a new container.";
|
|
13
|
+
static strict = false;
|
|
14
|
+
// Usage needs to be overwritten because the autogenerated one is incorrect due to the variadic arguments.
|
|
15
|
+
static usage = "container run [--token <value>] [-q] [-p <value>] [-e <value>...] [--env-file <value>...] [--description <value>] [--entrypoint <value>] [--name <value>] [-p <value>...] [-P] [-v <value>...] IMAGE [COMMAND] [ARGS...]";
|
|
13
16
|
static flags = {
|
|
14
17
|
...processFlags,
|
|
15
18
|
...projectFlags,
|
|
@@ -45,11 +48,11 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
45
48
|
summary: "publish a container's port(s) to the host",
|
|
46
49
|
description: "Map a container's port to a port on the host system. " +
|
|
47
50
|
"Format: <host-port>:<container-port> or just <container-port> (in which case the host port will be automatically assigned). " +
|
|
48
|
-
"For example,
|
|
49
|
-
"Use multiple
|
|
51
|
+
"For example, --publish 8080:80 maps port 80 in the container to port 8080 on the host. " +
|
|
52
|
+
"Use multiple --publish flags to publish multiple ports.\n\n" +
|
|
53
|
+
"NOTE: Please note that the usual shorthand -p is not supported for this flag, as it would conflict with the --project flag.",
|
|
50
54
|
required: false,
|
|
51
55
|
multiple: true,
|
|
52
|
-
char: "p",
|
|
53
56
|
}),
|
|
54
57
|
"publish-all": Flags.boolean({
|
|
55
58
|
summary: "publish all ports that are defined in the image",
|
|
@@ -85,7 +88,6 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
85
88
|
summary: "arguments to pass to the command",
|
|
86
89
|
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'.",
|
|
87
90
|
required: false,
|
|
88
|
-
variadic: true,
|
|
89
91
|
}),
|
|
90
92
|
};
|
|
91
93
|
async exec() {
|
|
@@ -116,6 +118,23 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
116
118
|
assertStatus(resp, 200);
|
|
117
119
|
return resp.data;
|
|
118
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds and returns the container command based on the provided image
|
|
123
|
+
* metadata and arguments.
|
|
124
|
+
*
|
|
125
|
+
* @param imageMeta The configuration object containing the metadata of the
|
|
126
|
+
* container image, including the default command.
|
|
127
|
+
* @returns An array of strings representing the container command to execute,
|
|
128
|
+
* or undefined if no specific command is set.
|
|
129
|
+
*/
|
|
130
|
+
buildContainerCommand(imageMeta) {
|
|
131
|
+
if (!this.args.command) {
|
|
132
|
+
return imageMeta.command;
|
|
133
|
+
}
|
|
134
|
+
const firstArg = (this.argv.lastIndexOf(this.args.command) ?? Infinity) + 1;
|
|
135
|
+
const command = [this.args.command, ...this.argv.slice(firstArg)];
|
|
136
|
+
return command;
|
|
137
|
+
}
|
|
119
138
|
/**
|
|
120
139
|
* Builds a container service request from command line arguments and image
|
|
121
140
|
* metadata
|
|
@@ -126,7 +145,7 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
126
145
|
* @returns A properly formatted container service request
|
|
127
146
|
*/
|
|
128
147
|
async buildServiceRequest(image, imageMeta, serviceName) {
|
|
129
|
-
const command = this.
|
|
148
|
+
const command = this.buildContainerCommand(imageMeta);
|
|
130
149
|
const entrypoint = this.flags.entrypoint
|
|
131
150
|
? [this.flags.entrypoint]
|
|
132
151
|
: imageMeta.entrypoint;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.js";
|
|
2
|
+
export default class Ssh extends ExtendedBaseCommand<typeof Ssh> {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static args: {
|
|
6
|
+
"container-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
info: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
test: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
shell: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
13
|
+
"ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as child_process from "child_process";
|
|
2
|
+
import { Args, Flags } from "@oclif/core";
|
|
3
|
+
import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.js";
|
|
4
|
+
import { getSSHConnectionForContainer } from "../../lib/resources/ssh/container.js";
|
|
5
|
+
import { sshConnectionFlags, } from "../../lib/resources/ssh/flags.js";
|
|
6
|
+
import { sshWrapperDocumentation } from "../../lib/resources/ssh/doc.js";
|
|
7
|
+
import { buildSSHClientFlags } from "../../lib/resources/ssh/connection.js";
|
|
8
|
+
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
9
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
10
|
+
export default class Ssh extends ExtendedBaseCommand {
|
|
11
|
+
static summary = "Connect to a container via SSH";
|
|
12
|
+
static description = "Establishes an interactive SSH connection to a container.\n\n" +
|
|
13
|
+
sshWrapperDocumentation;
|
|
14
|
+
static args = {
|
|
15
|
+
"container-id": Args.string({
|
|
16
|
+
description: "ID or short ID of the container to connect to",
|
|
17
|
+
required: true,
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
...sshConnectionFlags,
|
|
22
|
+
...projectFlags,
|
|
23
|
+
info: Flags.boolean({
|
|
24
|
+
summary: "only print connection information, without actually connecting",
|
|
25
|
+
}),
|
|
26
|
+
test: Flags.boolean({
|
|
27
|
+
summary: "test connection and exit",
|
|
28
|
+
}),
|
|
29
|
+
shell: Flags.string({
|
|
30
|
+
summary: "shell to use for the SSH connection",
|
|
31
|
+
default: "/bin/sh",
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { flags } = await this.parse(Ssh);
|
|
36
|
+
const [containerId, stackId] = await withContainerAndStackId(this.apiClient, Ssh, flags, this.args, this.config);
|
|
37
|
+
const { host, user, directory } = await getSSHConnectionForContainer(this.apiClient, containerId, stackId, flags["ssh-user"]);
|
|
38
|
+
if (flags.info) {
|
|
39
|
+
this.log("hostname: %o", host);
|
|
40
|
+
this.log("username: %o", user);
|
|
41
|
+
this.log("directory: %o", directory);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.log("connecting to %o as %o", host, user);
|
|
45
|
+
const [cmd, args] = buildSSHCmdAndFlags(user, host, flags);
|
|
46
|
+
this.debug("running ssh %o, with command %o", args, cmd);
|
|
47
|
+
child_process.spawnSync("/usr/bin/ssh", [...args, cmd], {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function buildSSHCmdAndFlags(user, host, flags) {
|
|
53
|
+
const args = buildSSHClientFlags(user, host, flags, {
|
|
54
|
+
interactive: true,
|
|
55
|
+
additionalFlags: flags.test ? ["-q"] : [],
|
|
56
|
+
});
|
|
57
|
+
if (flags.test) {
|
|
58
|
+
return ["/bin/true", args];
|
|
59
|
+
}
|
|
60
|
+
return [flags.shell, args];
|
|
61
|
+
}
|
|
@@ -6,6 +6,7 @@ import { Success } from "../../../rendering/react/components/Success.js";
|
|
|
6
6
|
import { dnsZoneArgs, withDnsZoneId, } from "../../../lib/resources/domain/dnszone/flags.js";
|
|
7
7
|
import { projectFlags } from "../../../lib/resources/project/flags.js";
|
|
8
8
|
import { assertStatus } from "@mittwald/api-client-commons";
|
|
9
|
+
import assertSuccess from "../../../lib/apiutil/assert_success.js";
|
|
9
10
|
export default class Update extends ExecRenderBaseCommand {
|
|
10
11
|
static description = "Updates a record set of a DNS zone";
|
|
11
12
|
static args = {
|
|
@@ -163,9 +164,9 @@ export default class Update extends ExecRenderBaseCommand {
|
|
|
163
164
|
dnsZoneId,
|
|
164
165
|
recordSet,
|
|
165
166
|
});
|
|
166
|
-
|
|
167
|
+
assertSuccess(r);
|
|
167
168
|
});
|
|
168
|
-
process.complete(_jsx(Success, { children: "DNS record set successfully reset to fully managed." }));
|
|
169
|
+
await process.complete(_jsx(Success, { children: "DNS record set successfully reset to fully managed." }));
|
|
169
170
|
}
|
|
170
171
|
render() {
|
|
171
172
|
return undefined;
|
|
@@ -5,6 +5,7 @@ import { CoreBaseCommand } from "./CoreBaseCommand.js";
|
|
|
5
5
|
import { configureAxiosLogging } from "../apiutil/api_logging.js";
|
|
6
6
|
import { configureAxiosRetry } from "../apiutil/api_retry.js";
|
|
7
7
|
import { configureConsistencyHandling } from "../apiutil/api_consistency.js";
|
|
8
|
+
import NoTokenFoundError from "../error/NoTokenFoundError.js";
|
|
8
9
|
/** Base command class for authenticated commands that includes the --token flag. */
|
|
9
10
|
export class BaseCommand extends CoreBaseCommand {
|
|
10
11
|
static baseFlags = {
|
|
@@ -21,7 +22,7 @@ export class BaseCommand extends CoreBaseCommand {
|
|
|
21
22
|
const { flags } = await this.parse();
|
|
22
23
|
const token = await this.getEffectiveTokenWithFlag(flags);
|
|
23
24
|
if (token === undefined) {
|
|
24
|
-
throw new
|
|
25
|
+
throw new NoTokenFoundError(getTokenFilename(this.config));
|
|
25
26
|
}
|
|
26
27
|
this.apiClient = MittwaldAPIV2Client.newWithToken(token);
|
|
27
28
|
this.apiClient.axios.defaults.headers["User-Agent"] =
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This class represents a custom error thrown when no token can be found. The
|
|
3
|
+
* error specifically occurs if the token is not provided via the --token flag,
|
|
4
|
+
* the MITTWALD_API_TOKEN environment variable, or the configuration file.
|
|
5
|
+
*
|
|
6
|
+
* This is a dedicated error class so that the error handler can check for this
|
|
7
|
+
* specific type of error and present it differently to the user.
|
|
8
|
+
*/
|
|
9
|
+
export default class NoTokenFoundError extends Error {
|
|
10
|
+
constructor(tokenFilename: string);
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This class represents a custom error thrown when no token can be found. The
|
|
3
|
+
* error specifically occurs if the token is not provided via the --token flag,
|
|
4
|
+
* the MITTWALD_API_TOKEN environment variable, or the configuration file.
|
|
5
|
+
*
|
|
6
|
+
* This is a dedicated error class so that the error handler can check for this
|
|
7
|
+
* specific type of error and present it differently to the user.
|
|
8
|
+
*/
|
|
9
|
+
export default class NoTokenFoundError extends Error {
|
|
10
|
+
constructor(tokenFilename) {
|
|
11
|
+
super(`Could not get token from --token flag, MITTWALD_API_TOKEN env var, or config file (${tokenFilename}). Please run "mw login token" or use --token.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
|
+
import { SSHConnectionData } from "./types.js";
|
|
3
|
+
export declare function getSSHConnectionForContainer(client: MittwaldAPIV2Client, containerId: string, stackId: string, sshUser: string | undefined): Promise<SSHConnectionData>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { assertStatus } from "@mittwald/api-client";
|
|
2
|
+
export async function getSSHConnectionForContainer(client, containerId, stackId, sshUser) {
|
|
3
|
+
// Get container details
|
|
4
|
+
const containerResponse = await client.container.getService({
|
|
5
|
+
stackId,
|
|
6
|
+
serviceId: containerId,
|
|
7
|
+
});
|
|
8
|
+
assertStatus(containerResponse, 200);
|
|
9
|
+
// Get project details
|
|
10
|
+
const projectResponse = await client.project.getProject({
|
|
11
|
+
projectId: containerResponse.data.projectId,
|
|
12
|
+
});
|
|
13
|
+
assertStatus(projectResponse, 200);
|
|
14
|
+
// If no SSH user is provided, use the current user's email
|
|
15
|
+
if (sshUser === undefined) {
|
|
16
|
+
const userResponse = await client.user.getUser({ userId: "self" });
|
|
17
|
+
assertStatus(userResponse, 200);
|
|
18
|
+
sshUser = userResponse.data.email;
|
|
19
|
+
}
|
|
20
|
+
// Construct the SSH connection data
|
|
21
|
+
const host = `ssh.${projectResponse.data.clusterID}.${projectResponse.data.clusterDomain}`;
|
|
22
|
+
const user = `${sshUser}@${containerResponse.data.shortId}`;
|
|
23
|
+
const directory = "/";
|
|
24
|
+
return {
|
|
25
|
+
host,
|
|
26
|
+
user,
|
|
27
|
+
directory,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Represents a mapping between a local port and a remote port. */
|
|
2
|
+
export default class PortMapping {
|
|
3
|
+
readonly localPort: number;
|
|
4
|
+
readonly remotePort: number;
|
|
5
|
+
constructor(localPort: number, remotePort: number);
|
|
6
|
+
private static validatePort;
|
|
7
|
+
static arg: import("@oclif/core/interfaces").ArgDefinition<PortMapping, Record<string, unknown>>;
|
|
8
|
+
/** @param str Port and protocol; example: `8080/tcp` */
|
|
9
|
+
static fromPortAndProtocol(str: string): PortMapping;
|
|
10
|
+
static fromString(str: string): PortMapping;
|
|
11
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
/** Represents a mapping between a local port and a remote port. */
|
|
3
|
+
export default class PortMapping {
|
|
4
|
+
localPort;
|
|
5
|
+
remotePort;
|
|
6
|
+
constructor(localPort, remotePort) {
|
|
7
|
+
this.localPort = localPort;
|
|
8
|
+
this.remotePort = remotePort;
|
|
9
|
+
}
|
|
10
|
+
static validatePort(port) {
|
|
11
|
+
return !isNaN(port) && port > 0 && port <= 65535;
|
|
12
|
+
}
|
|
13
|
+
static arg = Args.custom({
|
|
14
|
+
parse: async (input) => PortMapping.fromString(input),
|
|
15
|
+
});
|
|
16
|
+
/** @param str Port and protocol; example: `8080/tcp` */
|
|
17
|
+
static fromPortAndProtocol(str) {
|
|
18
|
+
const [localPort, protocol] = str.split("/");
|
|
19
|
+
const portNum = parseInt(localPort);
|
|
20
|
+
if (!PortMapping.validatePort(portNum)) {
|
|
21
|
+
throw new Error("Invalid port number. Ports must be between 1 and 65535.");
|
|
22
|
+
}
|
|
23
|
+
if (protocol.toLowerCase() !== "tcp") {
|
|
24
|
+
throw new Error("Only TCP protocol is supported.");
|
|
25
|
+
}
|
|
26
|
+
return new PortMapping(portNum, portNum);
|
|
27
|
+
}
|
|
28
|
+
static fromString(str) {
|
|
29
|
+
const [localPort, remotePort] = str.split(":");
|
|
30
|
+
const localPortNum = parseInt(localPort);
|
|
31
|
+
const remotePortNum = parseInt(remotePort);
|
|
32
|
+
if (!PortMapping.validatePort(localPortNum) ||
|
|
33
|
+
!PortMapping.validatePort(remotePortNum)) {
|
|
34
|
+
throw new Error("Invalid port number. Ports must be between 1 and 65535.");
|
|
35
|
+
}
|
|
36
|
+
return new PortMapping(localPortNum, remotePortNum);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import PortMapping from "./PortMapping.js";
|
|
3
|
+
describe("PortMapping", () => {
|
|
4
|
+
// Test: Successfully create PortMapping instance using fromString
|
|
5
|
+
it("should correctly parse valid port mapping string", () => {
|
|
6
|
+
const result = PortMapping.fromString("8080:9090");
|
|
7
|
+
expect(result.localPort).toBe(8080);
|
|
8
|
+
expect(result.remotePort).toBe(9090);
|
|
9
|
+
});
|
|
10
|
+
// Test: Throws an error for invalid local port
|
|
11
|
+
it("should throw an error for invalid local port", () => {
|
|
12
|
+
expect(() => PortMapping.fromString("100000:8080")).toThrow("Invalid port number. Ports must be between 1 and 65535.");
|
|
13
|
+
});
|
|
14
|
+
// Test: Throws an error for invalid remote port
|
|
15
|
+
it("should throw an error for invalid remote port", () => {
|
|
16
|
+
expect(() => PortMapping.fromString("8080:70000")).toThrow("Invalid port number. Ports must be between 1 and 65535.");
|
|
17
|
+
});
|
|
18
|
+
// Test: Throws an error when input format is incorrect
|
|
19
|
+
it("should throw an error for invalid string format", () => {
|
|
20
|
+
expect(() => PortMapping.fromString("8080-9090")).toThrow("Invalid port number. Ports must be between 1 and 65535.");
|
|
21
|
+
});
|
|
22
|
+
// Test: Successfully assign local and remote ports via constructor
|
|
23
|
+
it("should correctly initialize PortMapping with valid ports", () => {
|
|
24
|
+
const portMapping = new PortMapping(3000, 4000);
|
|
25
|
+
expect(portMapping.localPort).toBe(3000);
|
|
26
|
+
expect(portMapping.remotePort).toBe(4000);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -6,6 +6,7 @@ import GenericError from "./Error/GenericError.js";
|
|
|
6
6
|
import APIError from "./Error/APIError.js";
|
|
7
7
|
import UnexpectedShortIDPassedErrorBox from "./Error/UnexpectedShortIDPassedErrorBox.js";
|
|
8
8
|
import { MissingArgError, MissingFlagError, } from "../../../lib/context/FlagSetBuilder.js";
|
|
9
|
+
import NoTokenFoundError from "../../../lib/error/NoTokenFoundError.js";
|
|
9
10
|
/**
|
|
10
11
|
* Render an error to the terminal.
|
|
11
12
|
*
|
|
@@ -27,6 +28,9 @@ export const ErrorBox = ({ err }) => {
|
|
|
27
28
|
err instanceof MissingFlagError) {
|
|
28
29
|
return (_jsx(GenericError, { err: err, withStack: false, withIssue: false, title: "Input required" }));
|
|
29
30
|
}
|
|
31
|
+
else if (err instanceof NoTokenFoundError) {
|
|
32
|
+
return (_jsx(GenericError, { err: err, withStack: false, withIssue: false, title: "Login required" }));
|
|
33
|
+
}
|
|
30
34
|
else if (err instanceof UnexpectedShortIDPassedError) {
|
|
31
35
|
return _jsx(UnexpectedShortIDPassedErrorBox, { err: err });
|
|
32
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Hand-crafted CLI for the mittwald API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"semver-parser": "^4.1.6",
|
|
69
69
|
"shell-escape": "^0.2.0",
|
|
70
70
|
"slice-ansi": "^7.1.0",
|
|
71
|
-
"string-width": "^
|
|
71
|
+
"string-width": "^8.0.0",
|
|
72
72
|
"tempfile": "^5.0.0",
|
|
73
73
|
"uuid": "^11.0.3"
|
|
74
74
|
},
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"oclif": "^4.14.31",
|
|
95
95
|
"prettier": "~3.6.2",
|
|
96
96
|
"prettier-plugin-jsdoc": "^1.3.2",
|
|
97
|
-
"prettier-plugin-package": "^
|
|
97
|
+
"prettier-plugin-package": "^2.0.0",
|
|
98
98
|
"prettier-plugin-sort-json": "^4.1.1",
|
|
99
99
|
"rimraf": "^5.0.1",
|
|
100
100
|
"ts-jest": "^29.2.5",
|
|
@@ -117,6 +117,9 @@
|
|
|
117
117
|
"app": {
|
|
118
118
|
"description": "Manage apps, and app installations in your projects",
|
|
119
119
|
"subtopics": {
|
|
120
|
+
"create": {
|
|
121
|
+
"description": "Create new self-managed apps in your projects"
|
|
122
|
+
},
|
|
120
123
|
"install": {
|
|
121
124
|
"description": "Install apps in your projects"
|
|
122
125
|
},
|
|
@@ -128,9 +131,6 @@
|
|
|
128
131
|
"registry": {
|
|
129
132
|
"description": "Manage container registries"
|
|
130
133
|
},
|
|
131
|
-
"article": {
|
|
132
|
-
"description": "Query available hosting articles"
|
|
133
|
-
},
|
|
134
134
|
"backup": {
|
|
135
135
|
"description": "Manage backups of your projects",
|
|
136
136
|
"subtopics": {
|
|
@@ -139,12 +139,12 @@
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
},
|
|
142
|
+
"container": {
|
|
143
|
+
"description": "Manage containers"
|
|
144
|
+
},
|
|
142
145
|
"context": {
|
|
143
146
|
"description": "Save certain environment parameters for later use"
|
|
144
147
|
},
|
|
145
|
-
"contract": {
|
|
146
|
-
"description": "Manage your hosting contracts, and order new ones"
|
|
147
|
-
},
|
|
148
148
|
"conversation": {
|
|
149
149
|
"description": "Manage your support cases"
|
|
150
150
|
},
|
|
@@ -157,16 +157,42 @@
|
|
|
157
157
|
}
|
|
158
158
|
},
|
|
159
159
|
"cronjob": {
|
|
160
|
-
"description": "Manage cronjobs of your projects"
|
|
160
|
+
"description": "Manage cronjobs of your projects",
|
|
161
|
+
"subtopics": {
|
|
162
|
+
"execution": {
|
|
163
|
+
"description": "Manage individual cronjob executions"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
161
166
|
},
|
|
162
167
|
"database": {
|
|
163
|
-
"description": "Manage databases (like MySQL and Redis) in your projects"
|
|
168
|
+
"description": "Manage databases (like MySQL and Redis) in your projects",
|
|
169
|
+
"subtopics": {
|
|
170
|
+
"mysql": {
|
|
171
|
+
"description": "Manage MySQL databases in your projects",
|
|
172
|
+
"subtopics": {
|
|
173
|
+
"user": {
|
|
174
|
+
"description": "Manage MySQL database users"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"redis": {
|
|
179
|
+
"description": "Manage Redis databases in your projects"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
164
182
|
},
|
|
165
183
|
"ddev": {
|
|
166
184
|
"description": "Integrate your mittwald projects with DDEV"
|
|
167
185
|
},
|
|
168
186
|
"domain": {
|
|
169
|
-
"description": "Manage domains, virtual hosts and DNS settings in your projects"
|
|
187
|
+
"description": "Manage domains, virtual hosts and DNS settings in your projects",
|
|
188
|
+
"subtopics": {
|
|
189
|
+
"dnszone": {
|
|
190
|
+
"description": "Manage DNS zones for your domains"
|
|
191
|
+
},
|
|
192
|
+
"virtualhost": {
|
|
193
|
+
"description": "Manage virtual hosts for your domains"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
170
196
|
},
|
|
171
197
|
"extension": {
|
|
172
198
|
"description": "Install and manage extensions in your organisations and projects"
|
|
@@ -175,10 +201,26 @@
|
|
|
175
201
|
"description": "Manage your client authentication"
|
|
176
202
|
},
|
|
177
203
|
"mail": {
|
|
178
|
-
"description": "Manage mailboxes and mail addresses in your projects"
|
|
204
|
+
"description": "Manage mailboxes and mail addresses in your projects",
|
|
205
|
+
"subtopics": {
|
|
206
|
+
"address": {
|
|
207
|
+
"description": "Manage mail addresses in your projects"
|
|
208
|
+
},
|
|
209
|
+
"deliverybox": {
|
|
210
|
+
"description": "Manage mail delivery boxes in your projects"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
179
213
|
},
|
|
180
214
|
"org": {
|
|
181
|
-
"description": "Manage your organizations, and also any kinds of user memberships concerning these organizations."
|
|
215
|
+
"description": "Manage your organizations, and also any kinds of user memberships concerning these organizations.",
|
|
216
|
+
"subtopics": {
|
|
217
|
+
"invite": {
|
|
218
|
+
"description": "Invite users to your organizations and manage their invitations"
|
|
219
|
+
},
|
|
220
|
+
"membership": {
|
|
221
|
+
"description": "Control who gets to work in your organizations, and who doesn't"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
182
224
|
},
|
|
183
225
|
"project": {
|
|
184
226
|
"description": "Manage your projects, and also any kinds of user memberships concerning these projects.",
|
|
@@ -209,6 +251,12 @@
|
|
|
209
251
|
"user": {
|
|
210
252
|
"description": "Manage your own user account",
|
|
211
253
|
"subtopics": {
|
|
254
|
+
"api-token": {
|
|
255
|
+
"description": "Manage your API tokens"
|
|
256
|
+
},
|
|
257
|
+
"session": {
|
|
258
|
+
"description": "Manage your active sessions"
|
|
259
|
+
},
|
|
212
260
|
"ssh-key": {
|
|
213
261
|
"description": "Manage your SSH keys"
|
|
214
262
|
}
|