Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for uploading app files + databases #243

Merged
merged 21 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5d1b1ab
Add "app upload" + "mysql import" commands
martin-helmich Feb 15, 2024
b952e46
Add option to run "database mysql dump" command with --gzip flag
martin-helmich Feb 15, 2024
74f5b7d
Update README
martin-helmich Feb 15, 2024
957caa0
Remove "-x" flag from mysqldump shell command
martin-helmich Feb 15, 2024
c6aceac
Merge branches 'feature/ddev-support' and 'feature/mysqldump-gzip' in…
martin-helmich Feb 15, 2024
f5bbbbb
Extract temporary MySQL user management into its own function
martin-helmich Feb 15, 2024
a721b66
Remove leftover debug statement
martin-helmich Feb 15, 2024
59d9c8d
Extract "--[no-]temporary-user" flag
martin-helmich Feb 15, 2024
02e93d3
Merge remote-tracking branch 'origin/master' into feature/import-file…
martin-helmich Feb 27, 2024
7547d66
Add option to import gzipped dump
martin-helmich Feb 27, 2024
70cbf75
Fix documentation
martin-helmich Feb 27, 2024
9089fa5
Merge remote-tracking branch 'origin/master' into feature/import-file…
martin-helmich Feb 27, 2024
b3900ff
Merge remote-tracking branch 'origin/master' into feature/import-file…
martin-helmich Mar 7, 2024
5c7a108
Fix typo in comment
martin-helmich Apr 8, 2024
f2e6a6d
Use "mysql" instead of "mysqldump"
martin-helmich Apr 8, 2024
3bc3f9c
Merge remote-tracking branch 'origin/master' into feature/import-file…
martin-helmich Apr 8, 2024
981d4da
Refactor parameters of "executeViaSSH" function
martin-helmich Apr 8, 2024
59ed315
Make construction of mysql password parameter more explicit
martin-helmich Apr 8, 2024
d508120
Refactor functions for temporary user management
martin-helmich Apr 8, 2024
bcef63d
Make variable names more clear
martin-helmich Apr 9, 2024
c2b21c0
Rename "{with => runWith}ConnectionDetails" for clarity
martin-helmich Apr 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 105 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ USAGE
* [`mw app list`](#mw-app-list)
* [`mw app ssh [INSTALLATION-ID]`](#mw-app-ssh-installation-id)
* [`mw app uninstall [INSTALLATION-ID]`](#mw-app-uninstall-installation-id)
* [`mw app upload [INSTALLATION-ID]`](#mw-app-upload-installation-id)
* [`mw app versions [APP]`](#mw-app-versions-app)
* [`mw autocomplete [SHELL]`](#mw-autocomplete-shell)
* [`mw backup create`](#mw-backup-create)
Expand Down Expand Up @@ -167,6 +168,7 @@ USAGE
* [`mw database mysql delete DATABASE-ID`](#mw-database-mysql-delete-database-id)
* [`mw database mysql dump DATABASE-ID`](#mw-database-mysql-dump-database-id)
* [`mw database mysql get DATABASE-ID`](#mw-database-mysql-get-database-id)
* [`mw database mysql import DATABASE-ID`](#mw-database-mysql-import-database-id)
* [`mw database mysql list`](#mw-database-mysql-list)
* [`mw database mysql phpmyadmin DATABASE-ID`](#mw-database-mysql-phpmyadmin-database-id)
* [`mw database mysql port-forward DATABASE-ID`](#mw-database-mysql-port-forward-database-id)
Expand Down Expand Up @@ -1543,6 +1545,47 @@ FLAG DESCRIPTIONS
scripts), you can use this flag to easily get the IDs of created resources for further processing.
```

## `mw app upload [INSTALLATION-ID]`
martin-helmich marked this conversation as resolved.
Show resolved Hide resolved

Upload the filesystem of an app to a project

```
USAGE
$ mw app upload [INSTALLATION-ID] --source <value> [-q] [--ssh-user <value>] [--dry-run] [--delete]

ARGUMENTS
INSTALLATION-ID ID or short ID of an app installation; this argument is optional if a default app installation is set
in the context

FLAGS
-q, --quiet suppress process output and only display a machine-readable summary.
--delete delete remote files that are not present locally
--dry-run do not actually upload the app installation
--source=<value> (required) source directory from which to upload the app installation
martin-helmich marked this conversation as resolved.
Show resolved Hide resolved
LukasFritzeDev marked this conversation as resolved.
Show resolved Hide resolved
--ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used

DESCRIPTION
Upload the filesystem of an app to a project

Upload the filesystem of an app from your local machine to a project.

CAUTION: This is a potentially destructive operation. It will overwrite files on the server with the files from your
local machine. This is NOT a turnkey deployment solution. It is intended for development purposes only.

FLAG DESCRIPTIONS
-q, --quiet suppress process output and only display a machine-readable summary.

This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in
scripts), you can use this flag to easily get the IDs of created resources for further processing.

--ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used

This flag can be used to override the SSH user that is used for a connection; be default, your own personal user
will be used for this.

You can also set this value by setting the MITTWALD_SSH_USER environment variable.
```

## `mw app versions [APP]`

List supported Apps and Versions
Expand Down Expand Up @@ -2302,7 +2345,7 @@ Create a dump of a MySQL database

```
USAGE
$ mw database mysql dump DATABASE-ID -o <value> [-q] [-p <value>] [--ssh-user <value>] [--temporary-user] [--gzip]
$ mw database mysql dump DATABASE-ID -o <value> [-q] [-p <value>] [--temporary-user] [--ssh-user <value>] [--gzip]

ARGUMENTS
DATABASE-ID The ID or name of the database
Expand Down Expand Up @@ -2349,8 +2392,8 @@ FLAG DESCRIPTIONS

--[no-]temporary-user create a temporary user for the dump

Create a temporary user for the dump. This user will be deleted after the dump has been created. This is useful if
you want to dump a database that is not accessible from the outside.
Create a temporary user for this operation. This user will be deleted after the operation has completed. This is
useful if you want to work with a database that is not accessible from the outside.

If this flag is disabled, you will need to specify the password of the default user; either via the --mysql-password
flag or via the MYSQL_PWD environment variable.
Expand All @@ -2375,6 +2418,65 @@ DESCRIPTION
Get a MySQLDatabase.
```

## `mw database mysql import DATABASE-ID`

Imports a dump of a MySQL database

```
USAGE
$ mw database mysql import DATABASE-ID -i <value> [-q] [-p <value>] [--temporary-user] [--ssh-user <value>] [--gzip]
martin-helmich marked this conversation as resolved.
Show resolved Hide resolved

ARGUMENTS
DATABASE-ID The ID or name of the database

FLAGS
-i, --input=<value> (required) the input file from which to read the dump ("-" for stdin)
martin-helmich marked this conversation as resolved.
Show resolved Hide resolved
-p, --mysql-password=<value> the password to use for the MySQL user (env: MYSQL_PWD)
-q, --quiet suppress process output and only display a machine-readable summary.
--gzip uncompress the dump with gzip
--ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used
--[no-]temporary-user create a temporary user for the dump

FLAG DESCRIPTIONS
-i, --input=<value> the input file from which to read the dump ("-" for stdin)

The input file from which to read the dump to. You can specify "-" or "/dev/stdin" to read the dump directly from
STDIN.

-p, --mysql-password=<value> the password to use for the MySQL user (env: MYSQL_PWD)

The password to use for the MySQL user. If not provided, the environment variable MYSQL_PWD will be used. If that is
not set either, the command will interactively ask for the password.

NOTE: This is a security risk, as the password will be visible in the process list of your system, and will be
visible in your Shell history. It is recommended to use the environment variable instead.

-q, --quiet suppress process output and only display a machine-readable summary.

This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in
scripts), you can use this flag to easily get the IDs of created resources for further processing.

--gzip uncompress the dump with gzip

Uncompress the dump with gzip while importing. This is useful for large databases, as it can significantly reduce
the size of the dump.

--ssh-user=<value> override the SSH user to connect with; if omitted, your own user will be used

This flag can be used to override the SSH user that is used for a connection; be default, your own personal user
will be used for this.

You can also set this value by setting the MITTWALD_SSH_USER environment variable.

--[no-]temporary-user create a temporary user for the dump

Create a temporary user for this operation. This user will be deleted after the operation has completed. This is
useful if you want to work with a database that is not accessible from the outside.

If this flag is disabled, you will need to specify the password of the default user; either via the --mysql-password
flag or via the MYSQL_PWD environment variable.
```

## `mw database mysql list`

List MySQLDatabases belonging to a Project.
Expand Down
110 changes: 110 additions & 0 deletions src/commands/app/upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { ExecRenderBaseCommand } from "../../rendering/react/ExecRenderBaseCommand.js";
import { appInstallationArgs } from "../../lib/app/flags.js";
import {
makeProcessRenderer,
processFlags,
} from "../../rendering/process/process_flags.js";
import { Flags } from "@oclif/core";
import { Success } from "../../rendering/react/components/Success.js";
import { ReactNode } from "react";
import { hasBinary } from "../../lib/hasbin.js";
import { getSSHConnectionForAppInstallation } from "../../lib/ssh/appinstall.js";
import { spawnInProcess } from "../../rendering/process/process_exec.js";
import { sshConnectionFlags } from "../../lib/ssh/flags.js";

export class Upload extends ExecRenderBaseCommand<typeof Upload, void> {
static summary = "Upload the filesystem of an app to a project";
static description =
"Upload the filesystem of an app from your local machine to a project.\n\n" +
"" +
"CAUTION: This is a potentially destructive operation. It will overwrite files on the server with the files from your local machine. " +
"This is NOT a turnkey deployment solution. It is intended for development purposes only.";
static args = {
...appInstallationArgs,
};
static flags = {
...processFlags,
...sshConnectionFlags,
"dry-run": Flags.boolean({
description: "do not actually upload the app installation",
default: false,
}),
delete: Flags.boolean({
description: "delete remote files that are not present locally",
default: false,
}),
source: Flags.directory({
description: "source directory from which to upload the app installation",
required: true,
exists: true,
}),
};

protected async exec(): Promise<void> {
const appInstallationId = await this.withAppInstallationId(Upload);
const {
"dry-run": dryRun,
source,
delete: deleteRemote,
"ssh-user": sshUser,
} = this.flags;

const p = makeProcessRenderer(this.flags, "Uploading app installation");

const { host, user, directory } = await p.runStep(
"getting connection data",
async () => {
return getSSHConnectionForAppInstallation(
this.apiClient,
appInstallationId,
sshUser,
);
},
);

await p.runStep("check if rsync is installed", async () => {
if (!(await hasBinary("rsync"))) {
throw new Error("this command requires rsync to be installed");
}
});

const rsyncOpts = [
"--archive",
"--recursive",
"--verbose",
"--progress",
"--exclude=typo3temp",
];
if (dryRun) {
rsyncOpts.push("--dry-run");
}
if (deleteRemote) {
rsyncOpts.push("--delete");
}

await spawnInProcess(
p,
"uploading app installation" + (dryRun ? " (dry-run)" : ""),
"rsync",
[...rsyncOpts, source, `${user}@${host}:${directory}/`],
);

await p.complete(<UploadSuccess dryRun={dryRun} />);
}

protected render(): ReactNode {
return undefined;
}
}

function UploadSuccess({ dryRun }: { dryRun: boolean }) {
if (dryRun) {
return (
<Success>
App would (probably) have successfully been uploaded. 🙂
</Success>
);
}

return <Success>App successfully uploaded; have fun! 🚀</Success>;
}
70 changes: 9 additions & 61 deletions src/commands/database/mysql/dump.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ import * as fs from "fs";
import { Success } from "../../../rendering/react/components/Success.js";
import {
mysqlArgs,
mysqlConnectionFlags,
mysqlConnectionFlagsWithTempUser,
withMySQLId,
} from "../../../lib/database/mysql/flags.js";
import { getConnectionDetailsWithPassword } from "../../../lib/database/mysql/connect.js";
import { assertStatus } from "@mittwald/api-client";
import { randomBytes } from "crypto";
import { getConnectionDetailsWithPasswordOrTemporaryUser } from "../../../lib/database/mysql/connect.js";
import { executeViaSSH, RunCommand } from "../../../lib/ssh/exec.js";
import assertSuccess from "../../../lib/assert_success.js";
import shellEscape from "shell-escape";
import { sshConnectionFlags } from "../../../lib/ssh/flags.js";

Expand All @@ -29,16 +26,8 @@ export class Dump extends ExecRenderBaseCommand<
static summary = "Create a dump of a MySQL database";
static flags = {
...processFlags,
...mysqlConnectionFlags,
...mysqlConnectionFlagsWithTempUser,
...sshConnectionFlags,
"temporary-user": Flags.boolean({
summary: "create a temporary user for the dump",
description:
"Create a temporary user for the dump. This user will be deleted after the dump has been created. This is useful if you want to dump a database that is not accessible from the outside.\n\nIf this flag is disabled, you will need to specify the password of the default user; either via the --mysql-password flag or via the MYSQL_PWD environment variable.",
default: true,
required: false,
allowNo: true,
}),
output: Flags.string({
char: "o",
summary: 'the output file to write the dump to ("-" for stdout)',
Expand All @@ -61,30 +50,14 @@ export class Dump extends ExecRenderBaseCommand<
const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
const p = makeProcessRenderer(this.flags, "Dumping a MySQL database");

const connectionDetails = await getConnectionDetailsWithPassword(
this.apiClient,
databaseId,
p,
this.flags,
);

if (this.flags["temporary-user"]) {
const [tempUser, tempPassword] = await p.runStep(
"creating a temporary database user",
() => this.createTemporaryUser(databaseId),
const connectionDetails =
await getConnectionDetailsWithPasswordOrTemporaryUser(
this.apiClient,
databaseId,
p,
this.flags,
);

p.addCleanup("removing temporary database user", async () => {
const r = await this.apiClient.database.deleteMysqlUser({
mysqlUserId: tempUser.id,
});
assertSuccess(r);
});

connectionDetails.user = tempUser.name;
connectionDetails.password = tempPassword;
}

const { project } = connectionDetails;
const mysqldumpArgs = buildMySqlDumpArgs(connectionDetails);

Expand Down Expand Up @@ -131,31 +104,6 @@ export class Dump extends ExecRenderBaseCommand<

return fs.createWriteStream(this.flags.output);
}

private async createTemporaryUser(
databaseId: string,
): Promise<[{ id: string; name: string }, string]> {
const password = randomBytes(32).toString("base64");
const createResponse = await this.apiClient.database.createMysqlUser({
mysqlDatabaseId: databaseId,
data: {
accessLevel: "full", // needed for "PROCESS" privilege
externalAccess: false,
password,
databaseId,
description: "Temporary user for exporting database",
},
});

assertStatus(createResponse, 201);

const userResponse = await this.apiClient.database.getMysqlUser({
mysqlUserId: createResponse.data.id,
});
assertStatus(userResponse, 200);

return [userResponse.data, password];
}
}

function DumpSuccess({
Expand Down
Loading