@mittwald/cli 1.13.2 → 1.13.4
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/dist/commands/app/download.js +1 -1
- package/dist/commands/app/exec.js +1 -0
- package/dist/commands/app/ssh.js +3 -2
- package/dist/commands/app/upload.js +1 -1
- package/dist/commands/container/cp.js +6 -1
- package/dist/commands/container/exec.js +1 -0
- package/dist/commands/container/port-forward.js +1 -0
- package/dist/commands/container/ssh.js +3 -2
- package/dist/commands/container/update.js +1 -1
- package/dist/commands/database/mysql/create.js +1 -1
- package/dist/commands/database/mysql/dump.js +1 -1
- package/dist/commands/database/mysql/import.js +1 -1
- package/dist/commands/database/mysql/port-forward.js +1 -0
- package/dist/commands/database/mysql/shell.js +1 -0
- package/dist/commands/database/redis/shell.js +1 -0
- package/dist/commands/ddev/init.js +8 -4
- package/dist/commands/ddev/render-config.js +4 -1
- package/dist/commands/project/ssh.js +6 -1
- package/dist/lib/ddev/config_builder.d.ts +7 -1
- package/dist/lib/ddev/config_builder.js +39 -15
- package/dist/lib/ddev/config_builder.test.d.ts +1 -0
- package/dist/lib/ddev/config_builder.test.js +30 -0
- package/dist/lib/resources/app/sync.d.ts +1 -1
- package/dist/lib/resources/app/sync.js +6 -2
- package/dist/lib/resources/ssh/connection.d.ts +3 -1
- package/dist/lib/resources/ssh/connection.js +8 -2
- package/dist/lib/resources/ssh/exec.d.ts +1 -1
- package/dist/lib/resources/ssh/exec.js +2 -1
- package/dist/lib/resources/ssh/knownhosts.d.ts +36 -0
- package/dist/lib/resources/ssh/knownhosts.js +101 -0
- package/package.json +1 -1
|
@@ -54,7 +54,7 @@ export class Download extends ExecRenderBaseCommand {
|
|
|
54
54
|
});
|
|
55
55
|
const rsyncHost = buildRsyncConnectionString(connectionData, this.flags);
|
|
56
56
|
const rsyncOpts = [
|
|
57
|
-
...appInstallationSyncFlagsToRsyncFlags(this.flags),
|
|
57
|
+
...appInstallationSyncFlagsToRsyncFlags(this.flags, this.config.configDir),
|
|
58
58
|
...(await filterFileToRsyncFlagsIfPresent(target)),
|
|
59
59
|
];
|
|
60
60
|
await spawnInProcess(p, "downloading app installation" + (dryRun ? " (dry-run)" : ""), "rsync", [...rsyncOpts, rsyncHost, target]);
|
|
@@ -58,6 +58,7 @@ export default class Exec extends ExtendedBaseCommand {
|
|
|
58
58
|
execCommand += command;
|
|
59
59
|
const sshArgs = buildSSHClientFlags(user, host, flags, {
|
|
60
60
|
interactive: false,
|
|
61
|
+
configDir: this.config.configDir,
|
|
61
62
|
});
|
|
62
63
|
const wrappedExecCommand = shellEscape(["/bin/bash", "-c", execCommand]);
|
|
63
64
|
this.debug("running ssh %o, with command %o", sshArgs, wrappedExecCommand);
|
package/dist/commands/app/ssh.js
CHANGED
|
@@ -50,7 +50,7 @@ export default class Ssh extends ExtendedBaseCommand {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
this.log("connecting to %o as %o", host, user);
|
|
53
|
-
const [cmd, args] = buildSSHCmdAndFlags(user, host, directory, this.flags);
|
|
53
|
+
const [cmd, args] = buildSSHCmdAndFlags(user, host, directory, this.flags, this.config.configDir);
|
|
54
54
|
this.debug("running ssh %o, with command %o", args, cmd);
|
|
55
55
|
if (flags.cd) {
|
|
56
56
|
this.log("changing to %o; use --no-cd to disable this behaviour", directory);
|
|
@@ -60,10 +60,11 @@ export default class Ssh extends ExtendedBaseCommand {
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
function buildSSHCmdAndFlags(user, host, directory, flags) {
|
|
63
|
+
function buildSSHCmdAndFlags(user, host, directory, flags, configDir) {
|
|
64
64
|
const args = buildSSHClientFlags(user, host, flags, {
|
|
65
65
|
interactive: true,
|
|
66
66
|
additionalFlags: flags.test ? ["-q"] : [],
|
|
67
|
+
configDir,
|
|
67
68
|
});
|
|
68
69
|
if (flags.test) {
|
|
69
70
|
return ["/bin/true", args];
|
|
@@ -46,7 +46,7 @@ export class Upload extends ExecRenderBaseCommand {
|
|
|
46
46
|
});
|
|
47
47
|
const rsyncHost = buildRsyncConnectionString(connectionData, this.flags);
|
|
48
48
|
const rsyncOpts = [
|
|
49
|
-
...appInstallationSyncFlagsToRsyncFlags(this.flags),
|
|
49
|
+
...appInstallationSyncFlagsToRsyncFlags(this.flags, this.config.configDir),
|
|
50
50
|
...(await filterFileToRsyncFlagsIfPresent(source)),
|
|
51
51
|
];
|
|
52
52
|
await spawnInProcess(p, "uploading app installation" + (dryRun ? " (dry-run)" : ""), "rsync", [...rsyncOpts, source, rsyncHost]);
|
|
@@ -4,6 +4,7 @@ import { Args, Flags } from "@oclif/core";
|
|
|
4
4
|
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
5
5
|
import { getSSHConnectionForContainer } from "../../lib/resources/ssh/container.js";
|
|
6
6
|
import { sshConnectionFlags } from "../../lib/resources/ssh/flags.js";
|
|
7
|
+
import { getSSHKnownHostsFlags } from "../../lib/resources/ssh/knownhosts.js";
|
|
7
8
|
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
8
9
|
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
9
10
|
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
@@ -60,7 +61,11 @@ Where CONTAINER can be a container ID, short ID, or service name.`;
|
|
|
60
61
|
return { container, path: normalizedPath };
|
|
61
62
|
}
|
|
62
63
|
buildScpCommand(source, dest, flags) {
|
|
63
|
-
const scpArgs = [
|
|
64
|
+
const scpArgs = [
|
|
65
|
+
...getSSHKnownHostsFlags(this.config.configDir),
|
|
66
|
+
"-o",
|
|
67
|
+
"PasswordAuthentication=no",
|
|
68
|
+
];
|
|
64
69
|
if (flags.archive) {
|
|
65
70
|
scpArgs.push("-p");
|
|
66
71
|
}
|
|
@@ -69,6 +69,7 @@ export default class Exec extends ExtendedBaseCommand {
|
|
|
69
69
|
execCommand += command;
|
|
70
70
|
const sshArgs = buildSSHClientFlags(user, host, flags, {
|
|
71
71
|
interactive: false,
|
|
72
|
+
configDir: this.config.configDir,
|
|
72
73
|
});
|
|
73
74
|
const wrappedExecCommand = shellEscape([flags.shell, "-c", execCommand]);
|
|
74
75
|
this.debug("running ssh %o, with command %o", sshArgs, wrappedExecCommand);
|
|
@@ -44,6 +44,7 @@ export class PortForward extends ExecRenderBaseCommand {
|
|
|
44
44
|
additionalFlags: portMappings
|
|
45
45
|
.map((p) => ["-L", `${p.localPort}:localhost:${p.remotePort}`])
|
|
46
46
|
.flat(),
|
|
47
|
+
configDir: this.config.configDir,
|
|
47
48
|
});
|
|
48
49
|
cp.spawnSync("ssh", [...sshArgs, "sleep", "infinity"], {
|
|
49
50
|
stdio: ["ignore", process.stdout, process.stderr],
|
|
@@ -42,17 +42,18 @@ export default class Ssh extends ExtendedBaseCommand {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
this.log("connecting to %o as %o", host, user);
|
|
45
|
-
const [cmd, args] = buildSSHCmdAndFlags(user, host, flags);
|
|
45
|
+
const [cmd, args] = buildSSHCmdAndFlags(user, host, flags, this.config.configDir);
|
|
46
46
|
this.debug("running ssh %o, with command %o", args, cmd);
|
|
47
47
|
spawnSync("/usr/bin/ssh", [...args, cmd], {
|
|
48
48
|
stdio: "inherit",
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
function buildSSHCmdAndFlags(user, host, flags) {
|
|
52
|
+
function buildSSHCmdAndFlags(user, host, flags, configDir) {
|
|
53
53
|
const args = buildSSHClientFlags(user, host, flags, {
|
|
54
54
|
interactive: true,
|
|
55
55
|
additionalFlags: flags.test ? ["-q"] : [],
|
|
56
|
+
configDir,
|
|
56
57
|
});
|
|
57
58
|
if (flags.test) {
|
|
58
59
|
return ["/bin/true", args];
|
|
@@ -65,7 +65,7 @@ export class Update extends ExecRenderBaseCommand {
|
|
|
65
65
|
}),
|
|
66
66
|
volume: Flags.string({
|
|
67
67
|
summary: "update volume mounts for the container",
|
|
68
|
-
description: "This flag can be used to
|
|
68
|
+
description: "This flag can be used to replace volume mounts of the container. It can be used multiple times to mount multiple volumes." +
|
|
69
69
|
"" +
|
|
70
70
|
"Needs to be in the format <host-path>:<container-path>. " +
|
|
71
71
|
"" +
|
|
@@ -47,7 +47,7 @@ export class Dump extends ExecRenderBaseCommand {
|
|
|
47
47
|
shell: `set -e -o pipefail > /dev/null ; mysqldump ${escapedArgs} | gzip`,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
await p.runStep(`starting mysqldump via SSH on project ${project.shortId}`, () => executeViaSSH(this.apiClient, this.flags, { projectId: connectionDetails.project.id }, cmd, { input: null, output: this.getOutputStream() }));
|
|
50
|
+
await p.runStep(`starting mysqldump via SSH on project ${project.shortId}`, () => executeViaSSH(this.apiClient, this.flags, { projectId: connectionDetails.project.id }, cmd, { input: null, output: this.getOutputStream() }, this.config.configDir));
|
|
51
51
|
return connectionDetails.database;
|
|
52
52
|
});
|
|
53
53
|
await p.complete(_jsx(DumpSuccess, { database: databaseName, output: this.flags.output }));
|
|
@@ -47,7 +47,7 @@ export class Import extends ExecRenderBaseCommand {
|
|
|
47
47
|
shell: `set -e -o pipefail > /dev/null ; gunzip | mysql ${escapedArgs}`,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
await p.runStep(`starting mysql via SSH on project ${project.shortId}`, () => executeViaSSH(this.apiClient, this.flags, { projectId: connectionDetails.project.id }, cmd, { input: this.getInputStream(), output: null }));
|
|
50
|
+
await p.runStep(`starting mysql via SSH on project ${project.shortId}`, () => executeViaSSH(this.apiClient, this.flags, { projectId: connectionDetails.project.id }, cmd, { input: this.getInputStream(), output: null }, this.config.configDir));
|
|
51
51
|
return connectionDetails.database;
|
|
52
52
|
});
|
|
53
53
|
await p.complete(_jsx(ImportSuccess, { database: databaseName, input: this.flags.input }));
|
|
@@ -32,6 +32,7 @@ export class PortForward extends ExecRenderBaseCommand {
|
|
|
32
32
|
const sshArgs = buildSSHClientFlags(sshUser, sshHost, this.flags, {
|
|
33
33
|
interactive: false,
|
|
34
34
|
additionalFlags: ["-L", `${port}:${hostname}:3306`],
|
|
35
|
+
configDir: this.config.configDir,
|
|
35
36
|
});
|
|
36
37
|
cp.spawnSync("ssh", [...sshArgs, "sleep", "infinity"], {
|
|
37
38
|
stdio: ["ignore", process.stdout, process.stderr],
|
|
@@ -25,6 +25,7 @@ export class Shell extends ExecRenderBaseCommand {
|
|
|
25
25
|
await p.complete(_jsx(Text, { children: "Starting MySQL shell -- get ready..." }));
|
|
26
26
|
const sshArgs = buildSSHClientFlags(sshUser, sshHost, this.flags, {
|
|
27
27
|
interactive: true,
|
|
28
|
+
configDir: this.config.configDir,
|
|
28
29
|
});
|
|
29
30
|
const mysqlArgs = ["-h", hostname, "-u", user, "-p" + password, database];
|
|
30
31
|
cp.spawnSync("ssh", [...sshArgs, "mysql", ...mysqlArgs], {
|
|
@@ -24,6 +24,7 @@ export class Shell extends ExecRenderBaseCommand {
|
|
|
24
24
|
await p.complete(_jsx(Text, { children: "Starting Redis shell -- get ready..." }));
|
|
25
25
|
const sshArgs = buildSSHClientFlags(sshUser, sshHost, this.flags, {
|
|
26
26
|
interactive: true,
|
|
27
|
+
configDir: this.config.configDir,
|
|
27
28
|
});
|
|
28
29
|
const redisArgs = ["-h", hostname];
|
|
29
30
|
cp.spawnSync("ssh", [...sshArgs, "redis-cli", ...redisArgs], {
|
|
@@ -163,12 +163,16 @@ export class Init extends ExecRenderBaseCommand {
|
|
|
163
163
|
}
|
|
164
164
|
async writeMittwaldConfiguration(r, appInstallationId, databaseId, projectType) {
|
|
165
165
|
const builder = new DDEVConfigBuilder(this.apiClient);
|
|
166
|
-
|
|
167
|
-
const
|
|
166
|
+
const { config, warnings } = await r.runStep("creating mittwald-specific DDEV configuration", async () => {
|
|
167
|
+
const result = await builder.build(appInstallationId, databaseId, projectType);
|
|
168
168
|
const configFile = path.join(".ddev", "config.mittwald.yaml");
|
|
169
|
-
await writeContentsToFile(configFile, renderDDEVConfig(appInstallationId, config));
|
|
170
|
-
return
|
|
169
|
+
await writeContentsToFile(configFile, renderDDEVConfig(appInstallationId, result.config));
|
|
170
|
+
return result;
|
|
171
171
|
});
|
|
172
|
+
for (const warning of warnings) {
|
|
173
|
+
r.addInfo(`Warning: ${warning}`);
|
|
174
|
+
}
|
|
175
|
+
return config;
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
async function writeContentsToFile(filename, data) {
|
|
@@ -19,7 +19,10 @@ export class RenderConfig extends ExtendedBaseCommand {
|
|
|
19
19
|
? "none"
|
|
20
20
|
: this.flags["database-id"];
|
|
21
21
|
const ddevConfigBuilder = new DDEVConfigBuilder(this.apiClient);
|
|
22
|
-
const ddevConfig = await ddevConfigBuilder.build(appInstallationId, databaseId, projectType);
|
|
22
|
+
const { config: ddevConfig, warnings } = await ddevConfigBuilder.build(appInstallationId, databaseId, projectType);
|
|
23
|
+
for (const warning of warnings) {
|
|
24
|
+
this.warn(warning);
|
|
25
|
+
}
|
|
23
26
|
this.log(renderDDEVConfig(appInstallationId, ddevConfig));
|
|
24
27
|
}
|
|
25
28
|
}
|
|
@@ -4,6 +4,7 @@ import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.
|
|
|
4
4
|
import { sshConnectionFlags } from "../../lib/resources/ssh/flags.js";
|
|
5
5
|
import { getSSHConnectionForProject } from "../../lib/resources/ssh/project.js";
|
|
6
6
|
import { sshWrapperDocumentation } from "../../lib/resources/ssh/doc.js";
|
|
7
|
+
import { buildSSHClientFlags } from "../../lib/resources/ssh/connection.js";
|
|
7
8
|
export default class Ssh extends ExtendedBaseCommand {
|
|
8
9
|
static summary = "Connect to a project via SSH";
|
|
9
10
|
static description = "Establishes an interactive SSH connection to a project.\n\n" +
|
|
@@ -16,7 +17,11 @@ export default class Ssh extends ExtendedBaseCommand {
|
|
|
16
17
|
const projectId = await this.withProjectId(Ssh);
|
|
17
18
|
const { user, host } = await getSSHConnectionForProject(this.apiClient, projectId, this.flags["ssh-user"]);
|
|
18
19
|
this.log("connecting to %o as %o", host, user);
|
|
19
|
-
|
|
20
|
+
const sshArgs = buildSSHClientFlags(user, host, this.flags, {
|
|
21
|
+
interactive: true,
|
|
22
|
+
configDir: this.config.configDir,
|
|
23
|
+
});
|
|
24
|
+
spawnSync("/usr/bin/ssh", sshArgs, {
|
|
20
25
|
stdio: "inherit",
|
|
21
26
|
});
|
|
22
27
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
2
2
|
import { DDEVConfig } from "./config.js";
|
|
3
|
+
export type DDEVConfigBuildResult = {
|
|
4
|
+
config: Partial<DDEVConfig>;
|
|
5
|
+
warnings: string[];
|
|
6
|
+
};
|
|
3
7
|
export declare class DDEVConfigBuilder {
|
|
4
8
|
private apiClient;
|
|
5
9
|
constructor(apiClient: MittwaldAPIV2Client);
|
|
6
|
-
build(appInstallationId: string, databaseId: string | undefined, type: string): Promise<
|
|
10
|
+
build(appInstallationId: string, databaseId: string | undefined, type: string): Promise<DDEVConfigBuildResult>;
|
|
7
11
|
private buildHooks;
|
|
8
12
|
private determineDocumentRoot;
|
|
9
13
|
private determineProjectType;
|
|
@@ -16,3 +20,5 @@ export declare class DDEVConfigBuilder {
|
|
|
16
20
|
private getAppVersion;
|
|
17
21
|
private getAppInstallation;
|
|
18
22
|
}
|
|
23
|
+
export declare function hasExtendedSupportSuffix(version: string): boolean;
|
|
24
|
+
export declare function stripPatchLevelVersion(version: string): string;
|
|
@@ -10,17 +10,21 @@ export class DDEVConfigBuilder {
|
|
|
10
10
|
const appInstallation = await this.getAppInstallation(appInstallationId);
|
|
11
11
|
const systemSoftwares = await this.buildSystemSoftwareVersionMap(appInstallation);
|
|
12
12
|
type = await this.determineProjectType(appInstallation, type);
|
|
13
|
+
const { version: phpVersion, warnings } = this.determinePHPVersion(systemSoftwares);
|
|
13
14
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
config: {
|
|
16
|
+
type,
|
|
17
|
+
webserver_type: "apache-fpm",
|
|
18
|
+
php_version: phpVersion,
|
|
19
|
+
database: await this.determineDatabaseVersion(databaseId),
|
|
20
|
+
docroot: await this.determineDocumentRoot(appInstallation),
|
|
21
|
+
web_environment: [
|
|
22
|
+
`MITTWALD_APP_INSTALLATION_ID=${appInstallation.shortId}`,
|
|
23
|
+
`MITTWALD_DATABASE_ID=${databaseId ?? ""}`,
|
|
24
|
+
],
|
|
25
|
+
hooks: this.buildHooks(type),
|
|
26
|
+
},
|
|
27
|
+
warnings,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
30
|
buildHooks(type) {
|
|
@@ -82,10 +86,22 @@ export class DDEVConfigBuilder {
|
|
|
82
86
|
}
|
|
83
87
|
determinePHPVersion(systemSoftwareVersions) {
|
|
84
88
|
if (!("php" in systemSoftwareVersions)) {
|
|
85
|
-
return undefined;
|
|
89
|
+
return { version: undefined, warnings: [] };
|
|
90
|
+
}
|
|
91
|
+
const originalVersion = systemSoftwareVersions["php"];
|
|
92
|
+
const normalizedVersion = stripPatchLevelVersion(originalVersion);
|
|
93
|
+
if (hasExtendedSupportSuffix(originalVersion)) {
|
|
94
|
+
return {
|
|
95
|
+
version: normalizedVersion,
|
|
96
|
+
warnings: [
|
|
97
|
+
`The PHP version used by this project (${originalVersion}) is an extended support version ` +
|
|
98
|
+
"that is not directly supported by DDEV. " +
|
|
99
|
+
`Falling back to PHP ${normalizedVersion}. ` +
|
|
100
|
+
"This may cause unintended side effects.",
|
|
101
|
+
],
|
|
102
|
+
};
|
|
86
103
|
}
|
|
87
|
-
|
|
88
|
-
return stripPatchLevelVersion(version);
|
|
104
|
+
return { version: normalizedVersion, warnings: [] };
|
|
89
105
|
}
|
|
90
106
|
async buildSystemSoftwareVersionMap(inst) {
|
|
91
107
|
const versionMap = {};
|
|
@@ -133,7 +149,15 @@ function hasCustomDocumentRoot(inst) {
|
|
|
133
149
|
function stripLeadingSlash(input) {
|
|
134
150
|
return input.replace(/^\//, "");
|
|
135
151
|
}
|
|
136
|
-
|
|
152
|
+
const extendedSupportSuffixPattern = /-es$/;
|
|
153
|
+
export function hasExtendedSupportSuffix(version) {
|
|
154
|
+
return extendedSupportSuffixPattern.test(version);
|
|
155
|
+
}
|
|
156
|
+
export function stripPatchLevelVersion(version) {
|
|
137
157
|
const [major, minor] = version.split(".");
|
|
138
|
-
|
|
158
|
+
// Strip any extended support suffix (e.g. "-es") from the minor version
|
|
159
|
+
const cleanMinor = minor
|
|
160
|
+
? minor.replace(extendedSupportSuffixPattern, "")
|
|
161
|
+
: minor;
|
|
162
|
+
return `${major}.${cleanMinor}`;
|
|
139
163
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { hasExtendedSupportSuffix, stripPatchLevelVersion, } from "./config_builder.js";
|
|
3
|
+
describe("hasExtendedSupportSuffix", () => {
|
|
4
|
+
it("returns true for extended support versions", () => {
|
|
5
|
+
expect(hasExtendedSupportSuffix("7.2-es")).toBe(true);
|
|
6
|
+
expect(hasExtendedSupportSuffix("7.1-es")).toBe(true);
|
|
7
|
+
expect(hasExtendedSupportSuffix("8.0-es")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it("returns false for regular versions", () => {
|
|
10
|
+
expect(hasExtendedSupportSuffix("7.2")).toBe(false);
|
|
11
|
+
expect(hasExtendedSupportSuffix("8.3")).toBe(false);
|
|
12
|
+
expect(hasExtendedSupportSuffix("7.2.1")).toBe(false);
|
|
13
|
+
expect(hasExtendedSupportSuffix("7.2-lts")).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe("stripPatchLevelVersion", () => {
|
|
17
|
+
it("strips patch level from regular versions", () => {
|
|
18
|
+
expect(stripPatchLevelVersion("7.2.5")).toBe("7.2");
|
|
19
|
+
expect(stripPatchLevelVersion("8.3.1")).toBe("8.3");
|
|
20
|
+
});
|
|
21
|
+
it("strips extended support suffix from versions", () => {
|
|
22
|
+
expect(stripPatchLevelVersion("7.2-es")).toBe("7.2");
|
|
23
|
+
expect(stripPatchLevelVersion("7.1-es")).toBe("7.1");
|
|
24
|
+
expect(stripPatchLevelVersion("8.0-es")).toBe("8.0");
|
|
25
|
+
});
|
|
26
|
+
it("returns major.minor unchanged for already-stripped versions", () => {
|
|
27
|
+
expect(stripPatchLevelVersion("7.2")).toBe("7.2");
|
|
28
|
+
expect(stripPatchLevelVersion("8.3")).toBe("8.3");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -26,4 +26,4 @@ export declare function filterFileToRsyncFlagsIfPresent(targetDir: string, filte
|
|
|
26
26
|
* sync
|
|
27
27
|
*/
|
|
28
28
|
export declare function buildRsyncConnectionString({ host, directory, user }: SSHConnectionData, { "remote-sub-directory": subDirectory }: AppInstallationSyncFlags): string;
|
|
29
|
-
export declare function appInstallationSyncFlagsToRsyncFlags(f: AppInstallationSyncFlags & SSHConnectionFlags): string[];
|
|
29
|
+
export declare function appInstallationSyncFlagsToRsyncFlags(f: AppInstallationSyncFlags & SSHConnectionFlags, configDir: string): string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { pathExists } from "../../util/fs/pathExists.js";
|
|
4
|
+
import { getSSHKnownHostsArgsString } from "../ssh/knownhosts.js";
|
|
4
5
|
export const defaultRsyncFilterFile = ".mw-rsync-filter";
|
|
5
6
|
export const appInstallationSyncFlags = (direction) => ({
|
|
6
7
|
exclude: Flags.string({
|
|
@@ -53,7 +54,7 @@ export function buildRsyncConnectionString({ host, directory, user }, { "remote-
|
|
|
53
54
|
}
|
|
54
55
|
return `${user}@${host}:${directory}/`;
|
|
55
56
|
}
|
|
56
|
-
export function appInstallationSyncFlagsToRsyncFlags(f) {
|
|
57
|
+
export function appInstallationSyncFlagsToRsyncFlags(f, configDir) {
|
|
57
58
|
const { "dry-run": dryRun, "ssh-identity-file": sshIdentityFile, exclude, } = f;
|
|
58
59
|
const rsyncOpts = [
|
|
59
60
|
"--archive",
|
|
@@ -68,9 +69,12 @@ export function appInstallationSyncFlagsToRsyncFlags(f) {
|
|
|
68
69
|
if (f.delete) {
|
|
69
70
|
rsyncOpts.push("--delete");
|
|
70
71
|
}
|
|
72
|
+
// Build the SSH command for rsync with known hosts and optional identity file
|
|
73
|
+
const sshArgs = [getSSHKnownHostsArgsString(configDir)];
|
|
71
74
|
if (sshIdentityFile) {
|
|
72
|
-
|
|
75
|
+
sshArgs.push(`-i ${sshIdentityFile}`);
|
|
73
76
|
}
|
|
77
|
+
rsyncOpts.push("--rsh", `ssh ${sshArgs.join(" ")}`);
|
|
74
78
|
if (exclude?.length > 0) {
|
|
75
79
|
rsyncOpts.push(...exclude.map((e) => `--exclude=${e}`));
|
|
76
80
|
}
|
|
@@ -7,6 +7,8 @@ export interface SSHClientFlagOptions {
|
|
|
7
7
|
interactive: boolean;
|
|
8
8
|
/** Additional flags to pass to the SSH client invocation. */
|
|
9
9
|
additionalFlags: string[];
|
|
10
|
+
/** The oclif config directory for storing the known_hosts file. */
|
|
11
|
+
configDir: string;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Builds the flags and arguments for an SSH client invocation.
|
|
@@ -19,4 +21,4 @@ export interface SSHClientFlagOptions {
|
|
|
19
21
|
* @param opts Additional (optional) options for the SSH client invocation.
|
|
20
22
|
* @see https://linux.die.net/man/1/ssh
|
|
21
23
|
*/
|
|
22
|
-
export declare function buildSSHClientFlags(username: string, hostname: string, inputFlags: SSHConnectionFlags, opts: Partial<SSHClientFlagOptions>): string[];
|
|
24
|
+
export declare function buildSSHClientFlags(username: string, hostname: string, inputFlags: SSHConnectionFlags, opts: Partial<SSHClientFlagOptions> & Pick<SSHClientFlagOptions, "configDir">): string[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getSSHKnownHostsFlags } from "./knownhosts.js";
|
|
1
2
|
/**
|
|
2
3
|
* Builds the flags and arguments for an SSH client invocation.
|
|
3
4
|
*
|
|
@@ -10,8 +11,13 @@
|
|
|
10
11
|
* @see https://linux.die.net/man/1/ssh
|
|
11
12
|
*/
|
|
12
13
|
export function buildSSHClientFlags(username, hostname, inputFlags, opts) {
|
|
13
|
-
const { interactive, additionalFlags = [] } = opts;
|
|
14
|
-
const flags = [
|
|
14
|
+
const { interactive, additionalFlags = [], configDir } = opts;
|
|
15
|
+
const flags = [
|
|
16
|
+
...getSSHKnownHostsFlags(configDir),
|
|
17
|
+
"-l",
|
|
18
|
+
username,
|
|
19
|
+
interactive ? "-t" : "-T",
|
|
20
|
+
];
|
|
15
21
|
if (inputFlags["ssh-identity-file"]) {
|
|
16
22
|
flags.push("-i", inputFlags["ssh-identity-file"]);
|
|
17
23
|
}
|
|
@@ -15,4 +15,4 @@ export interface RunIO {
|
|
|
15
15
|
input: NodeJS.ReadableStream | null;
|
|
16
16
|
output: NodeJS.WritableStream | null;
|
|
17
17
|
}
|
|
18
|
-
export declare function executeViaSSH(client: MittwaldAPIV2Client, sshConnectionFlags: SSHConnectionFlags, target: RunTarget, command: RunCommand, { input, output }: RunIO): Promise<void>;
|
|
18
|
+
export declare function executeViaSSH(client: MittwaldAPIV2Client, sshConnectionFlags: SSHConnectionFlags, target: RunTarget, command: RunCommand, { input, output }: RunIO, configDir: string): Promise<void>;
|
|
@@ -2,13 +2,14 @@ import cp from "child_process";
|
|
|
2
2
|
import { getSSHConnectionForAppInstallation } from "./appinstall.js";
|
|
3
3
|
import { getSSHConnectionForProject } from "./project.js";
|
|
4
4
|
import { buildSSHClientFlags } from "./connection.js";
|
|
5
|
-
export async function executeViaSSH(client, sshConnectionFlags, target, command, { input = null, output = null }) {
|
|
5
|
+
export async function executeViaSSH(client, sshConnectionFlags, target, command, { input = null, output = null }, configDir) {
|
|
6
6
|
const { user, host } = await connectionDataForTarget(client, target, sshConnectionFlags["ssh-user"]);
|
|
7
7
|
const sshCommandArgs = "shell" in command
|
|
8
8
|
? ["bash", "-c", command.shell]
|
|
9
9
|
: [command.command, ...command.args];
|
|
10
10
|
const sshArgs = buildSSHClientFlags(user, host, sshConnectionFlags, {
|
|
11
11
|
interactive: false,
|
|
12
|
+
configDir,
|
|
12
13
|
});
|
|
13
14
|
const ssh = cp.spawn("ssh", [...sshArgs, ...sshCommandArgs], {
|
|
14
15
|
stdio: [input ? "pipe" : "ignore", output ? "pipe" : "ignore", "pipe"],
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Returns the known hosts content in SSH known_hosts format. */
|
|
2
|
+
export declare function getKnownHostsContent(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Ensures that the mittwald known_hosts file exists and is up-to-date. Returns
|
|
5
|
+
* the path to the file.
|
|
6
|
+
*
|
|
7
|
+
* @param configDir The oclif config directory (typically ~/.config/mw on
|
|
8
|
+
* Linux/macOS)
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureKnownHostsFile(configDir: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Returns SSH command-line options to use the embedded known hosts.
|
|
13
|
+
*
|
|
14
|
+
* These options configure SSH to use our embedded known hosts file as a
|
|
15
|
+
* "global" known hosts file, which is checked in addition to the user's own
|
|
16
|
+
* ~/.ssh/known_hosts file.
|
|
17
|
+
*
|
|
18
|
+
* @param configDir The oclif config directory (typically ~/.config/mw on
|
|
19
|
+
* Linux/macOS)
|
|
20
|
+
*/
|
|
21
|
+
export declare function getSSHKnownHostsFlags(configDir: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Returns the SSH options string for use with rsync's --rsh flag or scp.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // For rsync:
|
|
27
|
+
* `--rsh "ssh ${getSSHKnownHostsArgsString(configDir)}"`;
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // For scp:
|
|
31
|
+
* `scp ${getSSHKnownHostsArgsString(configDir)} source dest`;
|
|
32
|
+
*
|
|
33
|
+
* @param configDir The oclif config directory (typically ~/.config/mw on
|
|
34
|
+
* Linux/macOS)
|
|
35
|
+
*/
|
|
36
|
+
export declare function getSSHKnownHostsArgsString(configDir: string): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Known SSH host keys for mittwald clusters.
|
|
5
|
+
*
|
|
6
|
+
* These host keys are embedded in the CLI to allow SSH connections to succeed
|
|
7
|
+
* even in CI/CD environments where the host keys are not pre-populated in
|
|
8
|
+
* ~/.ssh/known_hosts.
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/mittwald/cli/issues/1260
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Map of SSH hostnames to their RSA public keys. Format: "hostname" -> "ssh-rsa
|
|
14
|
+
* AAAA..."
|
|
15
|
+
*
|
|
16
|
+
* These keys are hardcoded to have an authentic source that is independent of
|
|
17
|
+
* the user's environment, so that SSH connections to mittwald clusters can
|
|
18
|
+
* succeed even if the user has not previously connected to those hosts and thus
|
|
19
|
+
* does not have their keys in their own known_hosts file. This is especially
|
|
20
|
+
* important for CI/CD environments where the user's home directory may not be
|
|
21
|
+
* persistent or pre-populated with known host keys.
|
|
22
|
+
*
|
|
23
|
+
* The hostnames used here (e.g. "ssh.fabbenstedt.project.host") are the ones
|
|
24
|
+
* that users will see in SSH connection strings, and they must match the
|
|
25
|
+
* hostnames used in the cluster connection instructions and the SSH config
|
|
26
|
+
* generated by the CLI.
|
|
27
|
+
*/
|
|
28
|
+
const knownHostKeys = {
|
|
29
|
+
"ssh.fabbenstedt.project.host": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3X+n//34lLBZQsxkqgs1BrUdbqpU3KeF9n+hF4XHIiq2tXm7w6mBVWZlebUjFzKyLMDsaz/1AnUajWPp5CjCA1brEGBrCVoC7nwgmsTw5CMN/66Kb/sr3DQLlqGl5ylv+hF9RVmcqKyBbkPIHCGJm1eG+rwEWX0QMNpTyeQxDzxBLTtvcYebgkrxNEo/bs7YvTFoR+yWHt2MqJMnVDbzy/sm0mZCOFgE/jZ2RwyGmPWat63cZIFWucTZ/C73dmwwOyX/RH9kUSaxBm77UTtNpM23dFLDYyh7dw9I0q/7beTNnMplIlo4YX5+mh2288qcFa6v7N/u/KvlzBmkRcRk7",
|
|
30
|
+
"ssh.schmalge.project.host": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8DvrhtCe2FQnMPf0caDgTTeyFYMjC/N6xJK9X7OkUgA0A5UPKYQm9/rurJsv9/1TziFYypGF+IMJT3X9/5O9DZMed2hPMmDjEJYYHXZdHT5vSeFf0WuSMWhDEQD757XVA+4ySbyw5+SKsF8HFtuao9moXYMijM+iHp3TWC8be3eJdGkooZundveGIK2xlcgTr3y2LfE9DFMvpx2q4WgDbLrrplvQdJE8eY3Vaz0o251c1PEoOkwCEGQdZxc9XzXHC+SoGV3YPx8WJqhqkM1ayxhOCKJohoN4Nm9H6fcl0tYJCX62+RPi7RRobPzmwbjre8lA2Ibfl3iJVvB830ipj",
|
|
31
|
+
"ssh.vehlage.project.host": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTi2M1JMT+yL8F6TVyMvVWpDKQJT9tIAfm32gt5FZoJBMI2NM0REh8eTrI0d5nTiaTQSFskKIXMP6kv1UONaFAWIeh6r5+l3GntyOxcakoE9hqsBzQECXyWGMbav47bkLG3JYaTJchlQeeaV05j53LYvVIjw3mdGQKhnhvyJ7T5TjGCZYU2vnzqCtdXHCXMKAcdfh0XtMLjHBn/GJOolH9jDIzjZo2WbwT5S11tZcOnARFoNxelgIuSHwepz+gbgAILEw8UxjwII0M6kXXM0XgyjL/blOjfbkgiy36qMhDwjsQvA2u336R+UfbONajvBs8heygdVmgPsVc5Uz1PqAD",
|
|
32
|
+
"ssh.gestringen.project.host": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCv2Ojw4TM0+jFyxUgd9EzAAb8mXIzG24yS4V0Md7yphrl9vgZOyEKFhy8KfXAN1qnT1lMQKwfRqthVnfQbbSBGLSrwF1ecbnNtcXX5eAPIP7ycI+PAJJsLmV2099DpXEh9bOrf+vai29OBwtrq1ukFXatocJbC4nKSPWaFuC6yPcgqTTGMZM3ZxVhl6ONtWyao/IoGgTIHiPGxvKqcmqYYU8Zs6VSnYqQ0Sp+v1S5lgECZsDZf+EvBUe53QW8hzqm7z54ef4Qwhn13ODXU7L75LbfKbe0NJdz5higqaiz9BuIMjfbOsBHSTau5suP5fg/3nWsB3KTjl+P1B4Ipow53",
|
|
33
|
+
"ssh.fiestel.project.host": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCmLLYqOR4nTxPpUbayAdhf8BxcbjCdzllS4ERhAOb8tmvm29dKrI9RAoFBwHYZL6fh/GnO3lIXpJI0hPtFifsi7WfdtPxK2rk/D8Ijgp19fqhuVQPnDp8WDbPU6dW7xqlWHsPXLIBdfLvPdgkuyC2FBUYG705P6n048DyC8RTJOA57LZ79W5MiaEfxxSqns1l5amhky1qEUmiuW20Y/QJ3aKxliz+Gw6jdSKfM66mAH/3+JhRyB4oGSPoR3IFTjiba6PLhUYzCj1J9lyGxrNAe8vlJeu/wxFjjCqAC0BIyWE4wuxZdjeCjgCKOFIQAuB3ECFg7Qda5tGLPgG58Ni31",
|
|
34
|
+
"ssh.frotheim.project.host": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDffzlp4b+mFwmVN2aZrm5pwYj/q6VU6P2NKQWsuft3wgroWbo74p2DAqxG904DrcSZJ1ZBbG9up8hBewbMDgMX5pr+A4Nq0nFS7C3+ctrFfpaRXTGOcqxwKlNlrkqhOHDTRvNcZoFd8DseX07YdM5E9vXcRrEFcO4MNuO6jEKtFXE5KRo4SzMUvHrFpDrL608uvv5LTJynkRGu9zrK8AMgURrIGs4GuhsE1/sSYdtu1+r5sMm4tgCrkfxgEi6weJVG4ZCap6tokg/JojMnExNNnhFvNQEg3QiXHMn4vPh45jldezusodhad51mILMPqSmHlDLwuB0KWmfLQidzT+6j",
|
|
35
|
+
"ssh.isenstedt.project.host": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDA4BkiBXadL4ZCixqcOywUp+l4RnzNEtTTC+Gr+w1dQkXuGg5b6RGJmu+KodFgMyOTPwQMnhj0y0ZQKeHSQVQ4xYLO4kAZNc5AgGPuR9a1cozdLisL8E52fl6YP0ytqOtuH/hsKoIskz1Zl8xUP6mtgVqOT3sZtG29kh3JhngP+JBw94yUs0bOIO84ZPpFbEQ9hmkHMrkHgVoCYpgbV5hnY7tOSyKxWVEQChgXwWe11vpmZzv4XZtnP39bwLbiy4mnOkGqLreXb7kCAljF9hqCOyTaC+mSDdAMsM+qdy7A4SHj6RqCd77QHkmzHJ9gBUnGNX8xMN7+9Rlz3qxK6bqD",
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Path to the mittwald CLI known_hosts file.
|
|
39
|
+
*
|
|
40
|
+
* @param configDir The oclif config directory (typically ~/.config/mw on
|
|
41
|
+
* Linux/macOS)
|
|
42
|
+
*/
|
|
43
|
+
function getKnownHostsFilePath(configDir) {
|
|
44
|
+
return path.join(configDir, "known_hosts");
|
|
45
|
+
}
|
|
46
|
+
/** Returns the known hosts content in SSH known_hosts format. */
|
|
47
|
+
export function getKnownHostsContent() {
|
|
48
|
+
return Object.entries(knownHostKeys)
|
|
49
|
+
.map(([host, key]) => `${host} ${key}`)
|
|
50
|
+
.join("\n");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Ensures that the mittwald known_hosts file exists and is up-to-date. Returns
|
|
54
|
+
* the path to the file.
|
|
55
|
+
*
|
|
56
|
+
* @param configDir The oclif config directory (typically ~/.config/mw on
|
|
57
|
+
* Linux/macOS)
|
|
58
|
+
*/
|
|
59
|
+
export function ensureKnownHostsFile(configDir) {
|
|
60
|
+
const filePath = getKnownHostsFilePath(configDir);
|
|
61
|
+
// Ensure the directory exists
|
|
62
|
+
if (!fs.existsSync(configDir)) {
|
|
63
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
64
|
+
}
|
|
65
|
+
// Write the known hosts file
|
|
66
|
+
const content = getKnownHostsContent();
|
|
67
|
+
fs.writeFileSync(filePath, content + "\n", { mode: 0o600 });
|
|
68
|
+
return filePath;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Returns SSH command-line options to use the embedded known hosts.
|
|
72
|
+
*
|
|
73
|
+
* These options configure SSH to use our embedded known hosts file as a
|
|
74
|
+
* "global" known hosts file, which is checked in addition to the user's own
|
|
75
|
+
* ~/.ssh/known_hosts file.
|
|
76
|
+
*
|
|
77
|
+
* @param configDir The oclif config directory (typically ~/.config/mw on
|
|
78
|
+
* Linux/macOS)
|
|
79
|
+
*/
|
|
80
|
+
export function getSSHKnownHostsFlags(configDir) {
|
|
81
|
+
const knownHostsFile = ensureKnownHostsFile(configDir);
|
|
82
|
+
return ["-o", `GlobalKnownHostsFile=${knownHostsFile}`];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns the SSH options string for use with rsync's --rsh flag or scp.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // For rsync:
|
|
89
|
+
* `--rsh "ssh ${getSSHKnownHostsArgsString(configDir)}"`;
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // For scp:
|
|
93
|
+
* `scp ${getSSHKnownHostsArgsString(configDir)} source dest`;
|
|
94
|
+
*
|
|
95
|
+
* @param configDir The oclif config directory (typically ~/.config/mw on
|
|
96
|
+
* Linux/macOS)
|
|
97
|
+
*/
|
|
98
|
+
export function getSSHKnownHostsArgsString(configDir) {
|
|
99
|
+
const knownHostsFile = ensureKnownHostsFile(configDir);
|
|
100
|
+
return `-o GlobalKnownHostsFile=${knownHostsFile}`;
|
|
101
|
+
}
|