@webship.co/workspace-cpanel 0.0.0-stage → 1.0.0-alpha1
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 +59 -2
- package/commands/cmd-tools-cpanel.sh +22 -0
- package/config/plugin.cpanel.settings.yml +9 -0
- package/config/settings.labels.yml +7 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/link.d.ts +11 -0
- package/dist/src/lib/link.js +42 -0
- package/dist/src/lib/link.js.map +1 -0
- package/dist/src/lib/provider.d.ts +10 -0
- package/dist/src/lib/provider.js +84 -0
- package/dist/src/lib/provider.js.map +1 -0
- package/features/cpanel.feature +20 -0
- package/package.json +76 -4
- package/scripts/args/arg-cpanel.sh +43 -0
- package/scripts/functions/fun-cpanel-tool.sh +173 -0
- package/scripts/functions/fun-cpanel.sh +171 -0
- package/scripts/host-install.sh +14 -0
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @webship.co/workspace-cpanel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
cPanel for the [Webship Workspace](https://github.com/webship/workspace) dashboard: the **CPANEL**
|
|
4
|
+
menu on every site row, a live-site chip on the project card, "Link to cPanel" when a new folder is
|
|
5
|
+
made, and the `cmd-tools-cpanel.sh` command in every site workspace. A provider of
|
|
6
|
+
[`@webship.co/workspace-hosting`](https://github.com/webship/workspace-hosting), which draws the menu,
|
|
7
|
+
the environments modal, the get-database-and-files flow and the New-folder section for every host.
|
|
8
|
+
|
|
9
|
+
cPanel has no CLI of its own, so the dashboard talks to the UAPI over HTTPS (port 2083) with an API token per account, and the pulls, the deploy and the log go over SSH (`ssh`, `rsync`, `drush sql:dump` in the remote docroot). The link is `user@host:/home/user/public_html`; the environments are the account's domains, each a live site.
|
|
10
|
+
|
|
11
|
+
## Use it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd ~/workspace
|
|
15
|
+
bash core/scripts/plugins.sh --require github:webship/workspace-cpanel
|
|
16
|
+
bash core/scripts/plugins.sh --activate cpanel
|
|
17
|
+
bash core/scripts/plugins.sh --install cpanel
|
|
18
|
+
bash core/scripts/plugins.sh --configure cpanel # core/config/plugin.cpanel.settings.yml
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The same four steps are buttons on the dashboard's **Settings → Plugins** page.
|
|
22
|
+
|
|
23
|
+
## The command
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ~/workspace/dev
|
|
27
|
+
bash cmd-tools-cpanel.sh <project> [verb]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Verb | What it does |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `--link` <value> | Link the project to a cPanel account and site: user@host:/remote/docroot (the account user, the server host, the site's document root on it). |
|
|
33
|
+
| `--push` | Deploy: rsync the local docroot to the remote document root (excludes sites/default/files, settings.php, .git, .ddev; deletes what is gone locally). Outward-facing: asks before syncing. |
|
|
34
|
+
| `--environment` <value> | The domain for THIS run (an account can serve several). Default: the one remembered by --select. |
|
|
35
|
+
| `--environments` | List the account's domains (UAPI DomainInfo::list_domains) and disk usage. |
|
|
36
|
+
| `--select` <value> | Remember which domain this project works against (in .cpanel/workspace.yml, git-excluded). Checked against the account's domains. |
|
|
37
|
+
| `--pull-db` | Dump the site's database over SSH (drush sql:dump in the remote docroot) and import it into DDEV. REPLACES the local database — the current one is exported to backups/ first. Client data. |
|
|
38
|
+
| `--pull-files` | rsync the remote sites/default/files into the local docroot (adds and overwrites, never deletes). Client data. |
|
|
39
|
+
| `--logs` | Show the last 50 lines of the site's error_log over SSH. |
|
|
40
|
+
| `--no-safety-backup` | Skip exporting the local database before --pull-db replaces it. |
|
|
41
|
+
| `--force` | Allow --link to relink a project that already points at a different account. |
|
|
42
|
+
|
|
43
|
+
The link lives in the project's `.cpanel/workspace.yml` (git-excluded); the dashboard reads it to know
|
|
44
|
+
the project is linked and which environment it works against.
|
|
45
|
+
|
|
46
|
+
## What the package holds
|
|
47
|
+
|
|
48
|
+
| Path | What it is |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `src/index.ts` | `register(api)`: `registerHosting(api, cpanelProvider(api))` |
|
|
51
|
+
| `src/lib/provider.ts` | The cPanel provider: the credentials, the environments and their URLs, the verbs, the pulls, the console |
|
|
52
|
+
| `src/lib/link.ts` | The link file reader |
|
|
53
|
+
| `commands/cmd-tools-cpanel.sh` | The per-workspace command |
|
|
54
|
+
| `scripts/functions/fun-cpanel.sh` | The provider's bash hooks (its API) |
|
|
55
|
+
| `scripts/functions/fun-cpanel-tool.sh` | The family's dispatcher: link file, environment selection, safety export, status |
|
|
56
|
+
| `scripts/args/arg-cpanel.sh` | Its argparse |
|
|
57
|
+
| `scripts/host-install.sh` | The host side: the CLI and the credential check |
|
|
58
|
+
| `config/plugin.cpanel.settings.yml` | The settings template |
|
|
59
|
+
|
|
60
|
+
The `workspacePlugin` block in `package.json` is the manifest the installer reads.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Installed by the cPanel plugin (plugins.sh --install cpanel); __WORKSPACE__ is the workspace it was installed into.
|
|
4
|
+
#
|
|
5
|
+
# The local ⇄ cloud bridge for one project on cPanel: show its link and environments, link it,
|
|
6
|
+
# bring a database and files down into DDEV, deploy, read logs.
|
|
7
|
+
|
|
8
|
+
# Bootstrap.
|
|
9
|
+
# Find the workspace tooling from this script, so a fresh clone needs no setup.
|
|
10
|
+
WORKSPACE_SCRIPTS="${WORKSPACE_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../core/scripts" && pwd)}";
|
|
11
|
+
source ${WORKSPACE_SCRIPTS}/bootstrap.sh || exit 1 ;
|
|
12
|
+
|
|
13
|
+
# Load workspace settings and extra lists.
|
|
14
|
+
eval $(parse_yaml ${WORKSPACE_CONFIG}/workspace.__WORKSPACE__.settings.yml);
|
|
15
|
+
|
|
16
|
+
ARGPARSE_DESCRIPTION="Show, link, pull from or deploy a __WORKSPACE__ project on cPanel"
|
|
17
|
+
# The args file comes with the plugin: core/scripts/plugins/cpanel/ is where plugins.sh --install puts it.
|
|
18
|
+
source ${WORKSPACE_SCRIPTS}/plugins/cpanel/args/arg-cpanel.sh || exit 1 ;
|
|
19
|
+
|
|
20
|
+
shift $#;
|
|
21
|
+
|
|
22
|
+
cpanel_project_tool ;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# cPanel — core/config/plugin.cpanel.settings.yml
|
|
2
|
+
#
|
|
3
|
+
# How the dashboard reaches cPanel accounts: the UAPI over HTTPS with an API token (cPanel → Security → Manage API Tokens), one token per account. Which account a project belongs to is in its link (host and user); the token for that account is looked up here by user.
|
|
4
|
+
# A map of cPanel user → API token (cPanel → Security → Manage API Tokens), one per account the dashboard lists domains for. Example: {webshipco: XXXX}. Injected only into cPanel jobs, never printed.
|
|
5
|
+
api_tokens:
|
|
6
|
+
# webshipco: CHANGE_ME
|
|
7
|
+
CHANGE_ME_USER: CHANGE_ME
|
|
8
|
+
# The private key the pulls and deploys use to reach the accounts over SSH (authorized in cPanel → Security → SSH Access). Default: ~/.ssh/id_ed25519.
|
|
9
|
+
ssh_key: ~/.ssh/id_ed25519
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Labels for core/config/plugin.cpanel.settings.yml, merged into the Settings page.
|
|
2
|
+
api_tokens:
|
|
3
|
+
label: cPanel API tokens by user
|
|
4
|
+
info: 'A map of cPanel user → API token (cPanel → Security → Manage API Tokens), one per account the dashboard lists domains for. Example: a nested map, one user per line. Injected only into cPanel jobs, never printed.'
|
|
5
|
+
ssh_key:
|
|
6
|
+
label: SSH key for the pulls
|
|
7
|
+
info: 'The private key the pulls and deploys use to reach the accounts over SSH (authorized in cPanel → Security → SSH Access). Default: ~/.ssh/id_ed25519.'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cPanel for Webship Workspace.
|
|
3
|
+
*
|
|
4
|
+
* The CPANEL menu on every site row, a live-site chip, the New-folder link option and the
|
|
5
|
+
* `/cpanel-site/<workspace>/<project>` jump: all of it is the hosting family's surface
|
|
6
|
+
* (@webship.co/workspace-hosting). This package is the cPanel provider plus what the installer places
|
|
7
|
+
* (the per-workspace command, its bash functions, the settings template, the host install).
|
|
8
|
+
*
|
|
9
|
+
* Everything the dashboard is asked for goes through `api`; this package requires nothing from
|
|
10
|
+
* the dashboard's own files.
|
|
11
|
+
*/
|
|
12
|
+
import type { PluginApi } from '@webship.co/workspace-plugin-api';
|
|
13
|
+
import { cpanelProvider } from './lib/provider';
|
|
14
|
+
export { cpanelProvider };
|
|
15
|
+
export { cpanelLinkOf } from './lib/link';
|
|
16
|
+
export declare function register(api: PluginApi): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cPanel for Webship Workspace.
|
|
4
|
+
*
|
|
5
|
+
* The CPANEL menu on every site row, a live-site chip, the New-folder link option and the
|
|
6
|
+
* `/cpanel-site/<workspace>/<project>` jump: all of it is the hosting family's surface
|
|
7
|
+
* (@webship.co/workspace-hosting). This package is the cPanel provider plus what the installer places
|
|
8
|
+
* (the per-workspace command, its bash functions, the settings template, the host install).
|
|
9
|
+
*
|
|
10
|
+
* Everything the dashboard is asked for goes through `api`; this package requires nothing from
|
|
11
|
+
* the dashboard's own files.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.cpanelLinkOf = exports.cpanelProvider = void 0;
|
|
15
|
+
exports.register = register;
|
|
16
|
+
const workspace_hosting_1 = require("@webship.co/workspace-hosting");
|
|
17
|
+
const provider_1 = require("./lib/provider");
|
|
18
|
+
Object.defineProperty(exports, "cpanelProvider", { enumerable: true, get: function () { return provider_1.cpanelProvider; } });
|
|
19
|
+
var link_1 = require("./lib/link");
|
|
20
|
+
Object.defineProperty(exports, "cpanelLinkOf", { enumerable: true, get: function () { return link_1.cpanelLinkOf; } });
|
|
21
|
+
function register(api) {
|
|
22
|
+
(0, workspace_hosting_1.registerHosting)(api, (0, provider_1.cpanelProvider)(api));
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAUH,4BAEC;AATD,qEAAgE;AAEhE,6CAAgD;AAEvC,+FAFA,yBAAc,OAEA;AACvB,mCAA0C;AAAjC,oGAAA,YAAY,OAAA;AAErB,SAAgB,QAAQ,CAAC,GAAc;IACrC,IAAA,mCAAe,EAAC,GAAG,EAAE,IAAA,yBAAc,EAAC,GAAG,CAAC,CAAC,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a project is linked to cPanel, and to what: the `.cpanel/workspace.yml` file that
|
|
3
|
+
* `cmd-tools-cpanel.sh --link` writes (git-excluded), plus the branch the local code is on.
|
|
4
|
+
*/
|
|
5
|
+
import type { HostingLink } from '@webship.co/workspace-hosting';
|
|
6
|
+
export interface cPanelLink extends HostingLink {
|
|
7
|
+
/** Every key of the link file, for the provider's own needs (region, resource group, host…). */
|
|
8
|
+
fields: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export declare function readLinkFile(projectDir: string): Record<string, string> | null;
|
|
11
|
+
export declare function cpanelLinkOf(projectDir: string): cPanelLink | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Whether a project is linked to cPanel, and to what: the `.cpanel/workspace.yml` file that
|
|
4
|
+
* `cmd-tools-cpanel.sh --link` writes (git-excluded), plus the branch the local code is on.
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.readLinkFile = readLinkFile;
|
|
11
|
+
exports.cpanelLinkOf = cpanelLinkOf;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
function readLinkFile(projectDir) {
|
|
15
|
+
let text;
|
|
16
|
+
try {
|
|
17
|
+
text = fs_1.default.readFileSync(path_1.default.join(projectDir, '.cpanel', 'workspace.yml'), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch (_) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const fields = {};
|
|
23
|
+
for (const line of text.split('\n')) {
|
|
24
|
+
const m = line.match(/^([a-z_]+):\s*(.*?)\s*$/);
|
|
25
|
+
if (m)
|
|
26
|
+
fields[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
27
|
+
}
|
|
28
|
+
return fields;
|
|
29
|
+
}
|
|
30
|
+
function cpanelLinkOf(projectDir) {
|
|
31
|
+
const fields = readLinkFile(projectDir);
|
|
32
|
+
if (!fields || !fields.id)
|
|
33
|
+
return null;
|
|
34
|
+
let branch = null;
|
|
35
|
+
try {
|
|
36
|
+
const head = fs_1.default.readFileSync(path_1.default.join(projectDir, '.git', 'HEAD'), 'utf8').trim();
|
|
37
|
+
branch = (head.match(/^ref: refs\/heads\/(.+)$/) || [])[1] || null;
|
|
38
|
+
}
|
|
39
|
+
catch (_) { /* not a git clone */ }
|
|
40
|
+
return { id: fields.id, env: fields.env || null, branch, fields };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","sourceRoot":"","sources":["../../../src/lib/link.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;AAWH,oCASC;AAED,oCASC;AA7BD,4CAAoB;AACpB,gDAAwB;AAQxB,SAAgB,YAAY,CAAC,UAAkB;IAC7C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QAAC,IAAI,GAAG,YAAE,CAAC,YAAY,CAAC,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC;IAAC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IACrH,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAChD,IAAI,CAAC;YAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAgB,YAAY,CAAC,UAAkB;IAC7C,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAE,CAAC,YAAY,CAAC,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACnF,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACrE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACrC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACpE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cPanel, as a hosting provider.
|
|
3
|
+
*
|
|
4
|
+
* The link is `.cpanel/workspace.yml` (user, host, remote docroot); the account's domains come from
|
|
5
|
+
* the UAPI over HTTPS with the account's API token from plugin.cpanel.settings.yml; the pulls, the
|
|
6
|
+
* deploy and the log go over SSH through cmd-tools-cpanel.sh in a screen session.
|
|
7
|
+
*/
|
|
8
|
+
import type { PluginApi } from '@webship.co/workspace-plugin-api';
|
|
9
|
+
import type { HostingProvider } from '@webship.co/workspace-hosting';
|
|
10
|
+
export declare function cpanelProvider(api: PluginApi): HostingProvider;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cPanel, as a hosting provider.
|
|
4
|
+
*
|
|
5
|
+
* The link is `.cpanel/workspace.yml` (user, host, remote docroot); the account's domains come from
|
|
6
|
+
* the UAPI over HTTPS with the account's API token from plugin.cpanel.settings.yml; the pulls, the
|
|
7
|
+
* deploy and the log go over SSH through cmd-tools-cpanel.sh in a screen session.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.cpanelProvider = cpanelProvider;
|
|
11
|
+
const workspace_hosting_1 = require("@webship.co/workspace-hosting");
|
|
12
|
+
const link_1 = require("./link");
|
|
13
|
+
function cpanelProvider(api) {
|
|
14
|
+
const settings = () => api.settings.read() || {};
|
|
15
|
+
/** The token of one account: `api_tokens` is a map by user. */
|
|
16
|
+
const tokenFor = (user) => {
|
|
17
|
+
const map = settings().api_tokens;
|
|
18
|
+
if (map && typeof map === 'object')
|
|
19
|
+
return (0, workspace_hosting_1.configured)(map[user]);
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
const jobEnv = () => {
|
|
23
|
+
const map = settings().api_tokens;
|
|
24
|
+
const any = map && typeof map === 'object' && Object.values(map).some((v) => (0, workspace_hosting_1.configured)(v));
|
|
25
|
+
return any ? { ...process.env, CPANEL_API_TOKENS: JSON.stringify(map), CPANEL_SSH_KEY: String(settings().ssh_key || '~/.ssh/id_ed25519') } : null;
|
|
26
|
+
};
|
|
27
|
+
async function environments(link, lib, env) {
|
|
28
|
+
const fields = link.fields || {};
|
|
29
|
+
const user = fields.user;
|
|
30
|
+
const host = fields.host;
|
|
31
|
+
const id = link.id;
|
|
32
|
+
const token = user ? tokenFor(user) : null;
|
|
33
|
+
if (!token)
|
|
34
|
+
return { id, rows: [], why: `no token for ${user || 'the account'}` };
|
|
35
|
+
const out = await lib.run('curl', ['-sS', '-k', '-m', '30', '-H', `Authorization: cpanel ${user}:${token}`, `https://${host}:2083/execute/DomainInfo/list_domains`], process.env.HOME, { timeoutMs: 40 * 1000, env });
|
|
36
|
+
if (!out.ok)
|
|
37
|
+
return { id, rows: [], why: 'list failed' };
|
|
38
|
+
let data;
|
|
39
|
+
try {
|
|
40
|
+
data = JSON.parse(String(out.stdout)).data || {};
|
|
41
|
+
}
|
|
42
|
+
catch (_) {
|
|
43
|
+
return { id, rows: [], why: 'list failed' };
|
|
44
|
+
}
|
|
45
|
+
const rows = [];
|
|
46
|
+
if (data.main_domain)
|
|
47
|
+
rows.push({ title: `${data.main_domain} (main)`, envId: data.main_domain, status: 'active', url: `https://${data.main_domain}` });
|
|
48
|
+
for (const d of [...(data.addon_domains || []), ...(data.sub_domains || [])])
|
|
49
|
+
rows.push({ title: d, envId: d, status: 'active', url: `https://${d}` });
|
|
50
|
+
for (const d of data.parked_domains || [])
|
|
51
|
+
rows.push({ title: `${d} (alias)`, envId: d, status: 'alias' });
|
|
52
|
+
return { id, rows };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
id: 'cpanel',
|
|
56
|
+
label: 'CPANEL',
|
|
57
|
+
title: 'cPanel',
|
|
58
|
+
script: 'cmd-tools-cpanel.sh',
|
|
59
|
+
detect: (projectDir) => (0, link_1.cpanelLinkOf)(projectDir),
|
|
60
|
+
linkHint: (project) => `bash cmd-tools-cpanel.sh ${project} --link user@host:/home/user/public_html`,
|
|
61
|
+
consoleUrl: (link) => { const h = link.fields && link.fields.host; return h ? `https://${h}:2083/` : null; },
|
|
62
|
+
jobEnv,
|
|
63
|
+
tokenHint: 'api_tokens (by cPanel user) in core/config/plugin.cpanel.settings.yml',
|
|
64
|
+
environments,
|
|
65
|
+
tools: {
|
|
66
|
+
status: { verb: 'cpanel', label: 'Link and domains', icon: 'info', args: [] },
|
|
67
|
+
logs: { verb: 'cpanel-logs', label: 'Last 50 lines of error_log', icon: 'file-text', args: ['--logs'] },
|
|
68
|
+
deploy: { verb: 'cpanel-push', label: 'Deploy with rsync…', icon: 'push', args: ['--push'] },
|
|
69
|
+
},
|
|
70
|
+
pull: {
|
|
71
|
+
db: true, files: true,
|
|
72
|
+
dbArgs: (e) => ['--environment', e, '--pull-db'],
|
|
73
|
+
filesArgs: (e) => ['--environment', e, '--pull-files'],
|
|
74
|
+
noSafetyBackupArg: '--no-safety-backup',
|
|
75
|
+
},
|
|
76
|
+
folder: {
|
|
77
|
+
idPattern: /^[a-z0-9_.-]+@[a-z0-9.-]+:\/.+$/i,
|
|
78
|
+
idPlaceholder: 'webshipco@server.example.com:/home/webshipco/public_html',
|
|
79
|
+
idHelp: 'A cPanel link is user@host:/remote/docroot — the account user, the server, and the site\'s document root on it.',
|
|
80
|
+
},
|
|
81
|
+
menuNote: 'The pulls, the deploy and the log go over SSH with the key in the settings; the API token only lists domains',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../src/lib/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Feature: The cPanel plugin on a project row
|
|
2
|
+
Once the plugin is activated and installed, every site row carries the CPANEL menu, and the Plugins
|
|
3
|
+
page lists the plugin with its four steps.
|
|
4
|
+
|
|
5
|
+
Scenario: A site row offers the CPANEL menu, captioned with its link state
|
|
6
|
+
Given I am on "/dev"
|
|
7
|
+
Then the element ".act-menu-cpanel" should be displayed
|
|
8
|
+
And I should see "CPANEL"
|
|
9
|
+
And the element ".act-menu-cpanel + .act-status" should be displayed
|
|
10
|
+
|
|
11
|
+
Scenario: An unlinked project's menu opens and says how to link it
|
|
12
|
+
Given I am on "/dev"
|
|
13
|
+
And I click on the element ".project-row .act-menu-cpanel button"
|
|
14
|
+
Then ".uk-dropdown.uk-open" should be visible
|
|
15
|
+
And I should see "Not linked to cPanel"
|
|
16
|
+
|
|
17
|
+
Scenario: The plugin is listed on the Plugins page
|
|
18
|
+
Given I am on "/settings/plugins"
|
|
19
|
+
Then I should see "cPanel"
|
|
20
|
+
And I should see "@webship.co/workspace-cpanel"
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship.co/workspace-cpanel",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-alpha1",
|
|
4
|
+
"description": "cPanel hosting for Webship Workspace: the CPANEL menu on every site row, the account's domains as live sites, get database and files over SSH, deploy with rsync, the error log, the cPanel console. A provider of @webship.co/workspace-hosting.",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"types": "dist/src/index.d.ts",
|
|
7
|
+
"license": "ISC",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/webship/workspace-cpanel.git"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/src",
|
|
14
|
+
"commands",
|
|
15
|
+
"config",
|
|
16
|
+
"features",
|
|
17
|
+
"scripts",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"prepare": "npm run build",
|
|
23
|
+
"test": "npm run build && node --test dist/tests/*.test.js",
|
|
24
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"workspacePlugin": {
|
|
27
|
+
"name": "cpanel",
|
|
28
|
+
"label": "cPanel",
|
|
29
|
+
"settingsFile": "plugin.cpanel.settings.yml",
|
|
30
|
+
"settingsTemplate": "config/plugin.cpanel.settings.yml",
|
|
31
|
+
"labels": "config/settings.labels.yml",
|
|
32
|
+
"legacySettingsKey": "cpanel",
|
|
33
|
+
"typeActions": {
|
|
34
|
+
"cpanel": [
|
|
35
|
+
"sites"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"commands": {
|
|
39
|
+
"types": [
|
|
40
|
+
"sites"
|
|
41
|
+
],
|
|
42
|
+
"workspaces": [
|
|
43
|
+
"dev",
|
|
44
|
+
"test",
|
|
45
|
+
"demos",
|
|
46
|
+
"sandboxes",
|
|
47
|
+
"projects"
|
|
48
|
+
],
|
|
49
|
+
"files": [
|
|
50
|
+
"commands/cmd-tools-cpanel.sh"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"functions": [
|
|
55
|
+
"scripts/functions/fun-cpanel.sh",
|
|
56
|
+
"scripts/functions/fun-cpanel-tool.sh"
|
|
57
|
+
],
|
|
58
|
+
"args": [
|
|
59
|
+
"scripts/args/arg-cpanel.sh"
|
|
60
|
+
],
|
|
61
|
+
"hostInstall": "scripts/host-install.sh"
|
|
62
|
+
},
|
|
63
|
+
"features": [
|
|
64
|
+
"features/cpanel.feature"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@webship.co/workspace-hosting": "^1.0.0-alpha1"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/node": "^24.0.0",
|
|
72
|
+
"@webship.co/workspace-plugin-api": "^1.0.0-alpha1",
|
|
73
|
+
"typescript": "^5.9.0"
|
|
74
|
+
},
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
argparse "$@" <<ARGEOF || exit 1
|
|
4
|
+
parser.add_argument('PROJECT_NAME',
|
|
5
|
+
help='The project directory in this workspace.')
|
|
6
|
+
parser.add_argument('-l', '--link',
|
|
7
|
+
default="_none_",
|
|
8
|
+
help='Link the project to a cPanel account and site: user@host:/remote/docroot (the account user, the server host, the site's document root on it).')
|
|
9
|
+
parser.add_argument('-p', '--push',
|
|
10
|
+
action='store_true',
|
|
11
|
+
default=False,
|
|
12
|
+
help='Deploy: rsync the local docroot to the remote document root (excludes sites/default/files, settings.php, .git, .ddev; deletes what is gone locally). Outward-facing: asks before syncing.')
|
|
13
|
+
parser.add_argument('-e', '--environment',
|
|
14
|
+
default="_none_",
|
|
15
|
+
help='The domain for THIS run (an account can serve several). Default: the one remembered by --select.')
|
|
16
|
+
parser.add_argument('-E', '--environments',
|
|
17
|
+
action='store_true',
|
|
18
|
+
default=False,
|
|
19
|
+
help='List the account's domains (UAPI DomainInfo::list_domains) and disk usage.')
|
|
20
|
+
parser.add_argument('-s', '--select',
|
|
21
|
+
default="_none_",
|
|
22
|
+
help='Remember which domain this project works against (in .cpanel/workspace.yml, git-excluded). Checked against the account's domains.')
|
|
23
|
+
parser.add_argument('-D', '--pull-db',
|
|
24
|
+
action='store_true',
|
|
25
|
+
default=False,
|
|
26
|
+
help='Dump the site's database over SSH (drush sql:dump in the remote docroot) and import it into DDEV. REPLACES the local database — the current one is exported to backups/ first. Client data.')
|
|
27
|
+
parser.add_argument('-F', '--pull-files',
|
|
28
|
+
action='store_true',
|
|
29
|
+
default=False,
|
|
30
|
+
help='rsync the remote sites/default/files into the local docroot (adds and overwrites, never deletes). Client data.')
|
|
31
|
+
parser.add_argument('-g', '--logs',
|
|
32
|
+
action='store_true',
|
|
33
|
+
default=False,
|
|
34
|
+
help='Show the last 50 lines of the site's error_log over SSH.')
|
|
35
|
+
parser.add_argument('-B', '--no-safety-backup',
|
|
36
|
+
action='store_true',
|
|
37
|
+
default=False,
|
|
38
|
+
help='Skip exporting the local database before --pull-db replaces it.')
|
|
39
|
+
parser.add_argument('-f', '--force',
|
|
40
|
+
action='store_true',
|
|
41
|
+
default=False,
|
|
42
|
+
help='Allow --link to relink a project that already points at a different account.')
|
|
43
|
+
ARGEOF
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# The cPanel tool's dispatcher: generated by the hosting family's scaffolder, the same for every
|
|
4
|
+
# provider. The provider's own hooks live in fun-cpanel.sh:
|
|
5
|
+
# ensure_cpanel the CLI and credentials are usable, or say how
|
|
6
|
+
# cpanel_do_link <project_path> <id> link the project (writes the link file through cpanel_link_set)
|
|
7
|
+
# cpanel_list_environments <id> one environment id per line (for --select's check)
|
|
8
|
+
# cpanel_do_environments <id> print the environments for a person
|
|
9
|
+
# cpanel_pull_db <project_path> <id> <env> dump the environment's database to stdout path given in $1… (see fun-cpanel.sh)
|
|
10
|
+
# cpanel_pull_files <project_path> <id> <env>
|
|
11
|
+
# cpanel_do_push <project_path> <id> <env> deploy
|
|
12
|
+
# cpanel_do_logs <project_path> <id> <env>
|
|
13
|
+
# cpanel_do_activities <project_path> <id>
|
|
14
|
+
# cpanel_status <project_path> <id> the closing status block
|
|
15
|
+
|
|
16
|
+
CPANEL_LINK_FILE=".cpanel/workspace.yml";
|
|
17
|
+
|
|
18
|
+
# One key of the link file, or nothing.
|
|
19
|
+
function cpanel_link_get() {
|
|
20
|
+
local project_path="$1" key="$2";
|
|
21
|
+
[ -f "${project_path}/${CPANEL_LINK_FILE}" ] || return 0 ;
|
|
22
|
+
grep -E "^${key}:" "${project_path}/${CPANEL_LINK_FILE}" | head -1 | sed -E 's/^[a-z_]+:[[:space:]]*//; s/^"//; s/"$//' ;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Set one key of the link file, creating it (git-excluded: it is this machine's, not the repo's).
|
|
26
|
+
function cpanel_link_set() {
|
|
27
|
+
local project_path="$1" key="$2" value="$3" file;
|
|
28
|
+
file="${project_path}/${CPANEL_LINK_FILE}";
|
|
29
|
+
mkdir -p "$(dirname "${file}")" ;
|
|
30
|
+
touch "${file}" ;
|
|
31
|
+
if grep -qE "^${key}:" "${file}" ; then
|
|
32
|
+
sed -i -E "s|^${key}:.*|${key}: ${value}|" "${file}" ;
|
|
33
|
+
else
|
|
34
|
+
printf '%s: %s\n' "${key}" "${value}" >> "${file}" ;
|
|
35
|
+
fi
|
|
36
|
+
if [ -d "${project_path}/.git" ] && ! grep -qx "/.cpanel/" "${project_path}/.git/info/exclude" 2>/dev/null ; then
|
|
37
|
+
mkdir -p "${project_path}/.git/info" && printf '/.cpanel/\n' >> "${project_path}/.git/info/exclude" ;
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cpanel_linked_id() { cpanel_link_get "$1" id ; }
|
|
42
|
+
function cpanel_selected_env() { cpanel_link_get "$1" env ; }
|
|
43
|
+
|
|
44
|
+
# Which environment a run acts on: --environment for this run, else the remembered selection, else
|
|
45
|
+
# nothing. Deliberately NOT defaulting to production: pulling live data because nobody said
|
|
46
|
+
# otherwise is exactly the accident worth preventing.
|
|
47
|
+
function cpanel_target_env() {
|
|
48
|
+
if [ ! "${ENVIRONMENT:-_none_}" == '_none_' ] ; then printf '%s' "${ENVIRONMENT}"; return 0 ; fi
|
|
49
|
+
cpanel_selected_env "$1";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Where this project serves from: Drupal's two conventions, plus the flat case.
|
|
53
|
+
function cpanel_docroot() {
|
|
54
|
+
for candidate in web docroot ; do
|
|
55
|
+
[ -d "$1/${candidate}" ] && { printf '%s' "${candidate}"; return 0 ; }
|
|
56
|
+
done
|
|
57
|
+
printf '%s' "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# The local database is replaced by a pull, so it is exported first unless --no-safety-backup.
|
|
61
|
+
function cpanel_safety_export() {
|
|
62
|
+
local project_path="$1" safety="$2";
|
|
63
|
+
if [ "${NO_SAFETY_BACKUP:-no}" == 'yes' ] ; then
|
|
64
|
+
echo "Skipping the local database export, as asked. The current local data will be lost.";
|
|
65
|
+
else
|
|
66
|
+
echo "Exporting the current local database first.";
|
|
67
|
+
(cd "${project_path}" && ddev export-db --file="${safety}") \
|
|
68
|
+
|| echo " (no local database to export yet — continuing)";
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cpanel_import_dump() {
|
|
73
|
+
local project_path="$1" dump="$2";
|
|
74
|
+
echo "Importing it into DDEV.";
|
|
75
|
+
(cd "${project_path}" && ddev start -y > /dev/null && ddev import-db --file="${dump}") || return 1 ;
|
|
76
|
+
echo " Drupal sites usually need: ddev drush updb -y && ddev drush cr";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function cpanel_project_tool() {
|
|
80
|
+
local project_path="${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}";
|
|
81
|
+
local linked_id="";
|
|
82
|
+
|
|
83
|
+
if [ ! -d "${project_path}" ]; then
|
|
84
|
+
echo "No such project: ${project_path}";
|
|
85
|
+
return 1 ;
|
|
86
|
+
fi
|
|
87
|
+
cd "${project_path}" ;
|
|
88
|
+
linked_id="$(cpanel_linked_id "${project_path}")" ;
|
|
89
|
+
|
|
90
|
+
if [ ! "${LINK:-_none_}" == '_none_' ] ; then
|
|
91
|
+
ensure_cpanel || return 1 ;
|
|
92
|
+
if [ -n "${linked_id}" ] && [ ! "${linked_id}" == "${LINK}" ] && [ ! "${FORCE:-no}" == 'yes' ] ; then
|
|
93
|
+
echo "${doc_name}/${PROJECT_NAME} is already linked to cPanel ${linked_id}." ;
|
|
94
|
+
echo "Re-run with --force to relink it to ${LINK}." ;
|
|
95
|
+
return 1 ;
|
|
96
|
+
fi
|
|
97
|
+
cpanel_do_link "${project_path}" "${LINK}" || return 1 ;
|
|
98
|
+
linked_id="$(cpanel_linked_id "${project_path}")" ;
|
|
99
|
+
echo "Linked ${doc_name}/${PROJECT_NAME} → ${linked_id}." ;
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
local needs_link="";
|
|
103
|
+
for flag in "${ENVIRONMENTS:-no}" "${PULL_DB:-no}" "${PULL_FILES:-no}" "${PUSH:-no}" "${LOGS:-no}" "${ACTIVITIES:-no}" ; do
|
|
104
|
+
[ "${flag}" == 'yes' ] && needs_link=yes ;
|
|
105
|
+
done
|
|
106
|
+
[ ! "${SELECT:-_none_}" == '_none_' ] && needs_link=yes ;
|
|
107
|
+
if [ -n "${needs_link}" ] ; then
|
|
108
|
+
if [ -z "${linked_id}" ] ; then
|
|
109
|
+
echo "Not linked to cPanel — run with --link <id> first." ;
|
|
110
|
+
return 1 ;
|
|
111
|
+
fi
|
|
112
|
+
ensure_cpanel || return 1 ;
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [ "${ENVIRONMENTS:-no}" == 'yes' ] ; then
|
|
116
|
+
cpanel_do_environments "${project_path}" "${linked_id}" || return 1 ;
|
|
117
|
+
echo "";
|
|
118
|
+
echo " Select one: bash cmd-tools-cpanel.sh ${PROJECT_NAME} --select <environment>";
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [ ! "${SELECT:-_none_}" == '_none_' ] ; then
|
|
122
|
+
# Checked against the real list rather than taken on trust: a typo would otherwise be
|
|
123
|
+
# discovered by a pull that dumps the wrong environment, or none.
|
|
124
|
+
if ! cpanel_list_environments "${project_path}" "${linked_id}" 2>/dev/null | grep -qx "${SELECT}" ; then
|
|
125
|
+
echo "cPanel ${linked_id} has no environment called '${SELECT}'." ;
|
|
126
|
+
echo " List them: bash cmd-tools-cpanel.sh ${PROJECT_NAME} --environments" ;
|
|
127
|
+
return 1 ;
|
|
128
|
+
fi
|
|
129
|
+
cpanel_link_set "${project_path}" env "${SELECT}" ;
|
|
130
|
+
echo "${doc_name}/${PROJECT_NAME} now works against the ${SELECT} environment." ;
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [ "${PULL_DB:-no}" == 'yes' ] || [ "${PULL_FILES:-no}" == 'yes' ] || [ "${PUSH:-no}" == 'yes' ] ; then
|
|
134
|
+
local target_env;
|
|
135
|
+
target_env="$(cpanel_target_env "${project_path}")";
|
|
136
|
+
if [ -z "${target_env}" ] ; then
|
|
137
|
+
echo "No environment selected. Pick one first — a pull or a deploy must never guess which environment it targets:" ;
|
|
138
|
+
echo " bash cmd-tools-cpanel.sh ${PROJECT_NAME} --environments" ;
|
|
139
|
+
echo " bash cmd-tools-cpanel.sh ${PROJECT_NAME} --select <environment>" ;
|
|
140
|
+
echo " (or name it for this run: --environment <environment>)" ;
|
|
141
|
+
return 1 ;
|
|
142
|
+
fi
|
|
143
|
+
[ "${PULL_DB:-no}" == 'yes' ] && { cpanel_pull_db "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
|
|
144
|
+
[ "${PULL_FILES:-no}" == 'yes' ] && { cpanel_pull_files "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
|
|
145
|
+
[ "${PUSH:-no}" == 'yes' ] && { cpanel_do_push "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
if [ "${LOGS:-no}" == 'yes' ] ; then
|
|
149
|
+
cpanel_do_logs "${project_path}" "${linked_id}" "$(cpanel_target_env "${project_path}")" || true ;
|
|
150
|
+
fi
|
|
151
|
+
if [ "${ACTIVITIES:-no}" == 'yes' ] ; then
|
|
152
|
+
cpanel_do_activities "${project_path}" "${linked_id}" || true ;
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Default (and after any verb): status.
|
|
156
|
+
echo "";
|
|
157
|
+
echo "*---------------------------------------------------------------------*";
|
|
158
|
+
echo " ${doc_name}/${PROJECT_NAME} on cPanel";
|
|
159
|
+
if [ -z "${linked_id}" ]; then
|
|
160
|
+
echo " not linked. Link it with:";
|
|
161
|
+
echo " bash cmd-tools-cpanel.sh ${PROJECT_NAME} --link <id>";
|
|
162
|
+
cpanel_status "${project_path}" "" ;
|
|
163
|
+
else
|
|
164
|
+
echo " linked: ${linked_id}";
|
|
165
|
+
local selected;
|
|
166
|
+
selected="$(cpanel_selected_env "${project_path}")";
|
|
167
|
+
echo " env: ${selected:-none selected — --environments then --select <name>}";
|
|
168
|
+
cpanel_status "${project_path}" "${linked_id}" ;
|
|
169
|
+
fi
|
|
170
|
+
echo "*---------------------------------------------------------------------*";
|
|
171
|
+
|
|
172
|
+
cd "${WORKSPACE_ROOT}/${doc_name}";
|
|
173
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# cPanel accounts: the UAPI over HTTPS (port 2083, an API token per account) for the domains, and
|
|
4
|
+
# SSH for everything that touches the site (the database dump, the files, the deploy, the log). The
|
|
5
|
+
# hooks the hosting dispatcher (fun-cpanel-tool.sh) calls for one project directory. Everything
|
|
6
|
+
# defaults to read-only; --link, --push, --pull-db and --pull-files each demand their flag.
|
|
7
|
+
|
|
8
|
+
# The dashboard passes the tokens map as CPANEL_API_TOKENS (JSON by user) and the key as
|
|
9
|
+
# CPANEL_SSH_KEY; on the host they come from core/config/plugin.cpanel.settings.yml.
|
|
10
|
+
function cpanel_token_for() {
|
|
11
|
+
local user="$1";
|
|
12
|
+
if [ -n "${CPANEL_API_TOKENS:-}" ] ; then
|
|
13
|
+
printf '%s' "${CPANEL_API_TOKENS}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get(sys.argv[1],""))' "${user}" 2>/dev/null ;
|
|
14
|
+
else
|
|
15
|
+
# Either `api_tokens: {user: token}` on one line or a nested map under `api_tokens:`.
|
|
16
|
+
python3 - "${WORKSPACE_CONFIG}/plugin.cpanel.settings.yml" "${user}" <<'PY' 2>/dev/null
|
|
17
|
+
import re,sys,io
|
|
18
|
+
s=io.open(sys.argv[1],encoding='utf-8').read()
|
|
19
|
+
want=sys.argv[2]
|
|
20
|
+
m=re.search(r'^api_tokens:[ \t]*(\{.*\})[ \t]*$', s, re.M)
|
|
21
|
+
if m:
|
|
22
|
+
for pair in m.group(1).strip('{}').split(','):
|
|
23
|
+
if ':' in pair:
|
|
24
|
+
k,v=pair.split(':',1)
|
|
25
|
+
if k.strip().strip('"\'')==want: print(v.strip().strip('"\'')); break
|
|
26
|
+
else:
|
|
27
|
+
on=False
|
|
28
|
+
for line in s.split('\n'):
|
|
29
|
+
if re.match(r'^api_tokens:\s*$', line): on=True; continue
|
|
30
|
+
if on:
|
|
31
|
+
if line and not line.startswith(' '): break
|
|
32
|
+
mm=re.match(r'^\s+([^#\s:]+):\s*(.*?)\s*$', line)
|
|
33
|
+
if mm and mm.group(1)==want: print(mm.group(2).strip('"\'')); break
|
|
34
|
+
PY
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cpanel_ssh_key() {
|
|
39
|
+
local k="${CPANEL_SSH_KEY:-${ssh_key:-~/.ssh/id_ed25519}}";
|
|
40
|
+
printf '%s' "${k/#\~/${HOME}}";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensure_cpanel() {
|
|
44
|
+
for tool in ssh rsync curl python3 ; do
|
|
45
|
+
command -v "${tool}" > /dev/null 2>&1 || { echo "${tool} is not installed, and the cPanel tool needs it."; return 1; } ;
|
|
46
|
+
done
|
|
47
|
+
return 0 ;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# The link is user@host:/remote/docroot, kept as three keys; the id the dashboard shows is user@host.
|
|
51
|
+
function cpanel_do_link() {
|
|
52
|
+
local project_path="$1" spec="$2" user host remote;
|
|
53
|
+
[[ "${spec}" =~ ^([^@]+)@([^:]+):(/.+)$ ]] || { echo "A cPanel link is user@host:/remote/docroot, got: ${spec}"; return 1; } ;
|
|
54
|
+
user="${BASH_REMATCH[1]}"; host="${BASH_REMATCH[2]}"; remote="${BASH_REMATCH[3]}";
|
|
55
|
+
cpanel_link_set "${project_path}" id "${user}@${host}" ;
|
|
56
|
+
cpanel_link_set "${project_path}" user "${user}" ;
|
|
57
|
+
cpanel_link_set "${project_path}" host "${host}" ;
|
|
58
|
+
cpanel_link_set "${project_path}" remote "${remote}" ;
|
|
59
|
+
if ssh -i "$(cpanel_ssh_key)" -o BatchMode=yes -o ConnectTimeout=10 "${user}@${host}" "test -d '${remote}'" 2>/dev/null ; then
|
|
60
|
+
echo " SSH reaches ${user}@${host} and ${remote} exists." ;
|
|
61
|
+
else
|
|
62
|
+
echo " SSH could not reach ${user}@${host} with $(cpanel_ssh_key) (or ${remote} is missing): authorize the key in cPanel → Security → SSH Access." ;
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cpanel_uapi() {
|
|
67
|
+
local project_path="$1" call="$2" user host token;
|
|
68
|
+
user="$(cpanel_link_get "${project_path}" user)"; host="$(cpanel_link_get "${project_path}" host)";
|
|
69
|
+
token="$(cpanel_token_for "${user}")";
|
|
70
|
+
[ -z "${token}" ] && { echo "No API token for cPanel user ${user} in plugin.cpanel.settings.yml (api_tokens)." >&2; return 1; } ;
|
|
71
|
+
curl -sS -k -m 30 -H "Authorization: cpanel ${user}:${token}" "https://${host}:2083/execute/${call}" ;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cpanel_list_environments() {
|
|
75
|
+
cpanel_uapi "$1" DomainInfo/list_domains | python3 -c 'import json,sys
|
|
76
|
+
d=json.load(sys.stdin).get("data",{})
|
|
77
|
+
for x in [d.get("main_domain")]+d.get("addon_domains",[])+d.get("sub_domains",[]):
|
|
78
|
+
if x: print(x)' 2>/dev/null ;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cpanel_do_environments() {
|
|
82
|
+
local project_path="$1";
|
|
83
|
+
cpanel_uapi "${project_path}" DomainInfo/list_domains | python3 -c 'import json,sys
|
|
84
|
+
d=json.load(sys.stdin).get("data",{})
|
|
85
|
+
print(" %-40s %s" % (d.get("main_domain",""), "main https://"+d.get("main_domain","")))
|
|
86
|
+
for x in d.get("addon_domains",[]): print(" %-40s %s" % (x, "addon https://"+x))
|
|
87
|
+
for x in d.get("sub_domains",[]): print(" %-40s %s" % (x, "sub https://"+x))
|
|
88
|
+
for x in d.get("parked_domains",[]): print(" %-40s %s" % (x, "alias"))' || return 1 ;
|
|
89
|
+
cpanel_uapi "${project_path}" Quota/get_quota_info 2>/dev/null | python3 -c 'import json,sys
|
|
90
|
+
d=json.load(sys.stdin).get("data",{})
|
|
91
|
+
print(" disk: %s MB of %s MB" % (d.get("megabytes_used","?"), d.get("megabyte_limit","?")))' 2>/dev/null ;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cpanel_ssh() {
|
|
95
|
+
local project_path="$1"; shift;
|
|
96
|
+
ssh -i "$(cpanel_ssh_key)" -o BatchMode=yes -o ConnectTimeout=15 "$(cpanel_link_get "${project_path}" id)" "$@" ;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cpanel_pull_db() {
|
|
100
|
+
local project_path="$1" acct="$2" env="$3" stamp out dump safety remote;
|
|
101
|
+
remote="$(cpanel_link_get "${project_path}" remote)";
|
|
102
|
+
stamp="$(date +%Y-%m-%d-%H%M%S)";
|
|
103
|
+
out="${backups}/${doc_name}/${PROJECT_NAME}";
|
|
104
|
+
mkdir -p "${out}" ;
|
|
105
|
+
dump="${out}/${PROJECT_NAME}-cpanel-${env}-${stamp}.sql.gz";
|
|
106
|
+
safety="${out}/${PROJECT_NAME}-before-cpanel-pull-${stamp}.sql.gz";
|
|
107
|
+
echo "";
|
|
108
|
+
echo "Pulling the database of ${env} on ${acct} (drush sql:dump in ${remote}).";
|
|
109
|
+
echo " This is client data: it lands on this machine and in ${out}.";
|
|
110
|
+
echo "";
|
|
111
|
+
cpanel_safety_export "${project_path}" "${safety}" ;
|
|
112
|
+
echo "Dumping over SSH.";
|
|
113
|
+
cpanel_ssh "${project_path}" "cd '${remote}' && (command -v drush >/dev/null 2>&1 && drush sql:dump --gzip || ../vendor/bin/drush sql:dump --gzip)" > "${dump}" || return 1 ;
|
|
114
|
+
[ -s "${dump}" ] || { echo "The dump is empty — is drush on the server (vendor/bin/drush beside the docroot)?"; return 1; } ;
|
|
115
|
+
cpanel_import_dump "${project_path}" "${dump}" || return 1 ;
|
|
116
|
+
echo "";
|
|
117
|
+
echo " Imported ${env} into ${doc_name}/${PROJECT_NAME}.";
|
|
118
|
+
echo " pulled dump: ${dump}";
|
|
119
|
+
echo " local backup: ${safety}";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cpanel_pull_files() {
|
|
123
|
+
local project_path="$1" acct="$2" env="$3" docroot remote;
|
|
124
|
+
remote="$(cpanel_link_get "${project_path}" remote)";
|
|
125
|
+
docroot="$(cpanel_docroot "${project_path}")";
|
|
126
|
+
[ -z "${docroot}" ] && { echo "No web/ or docroot/ in ${doc_name}/${PROJECT_NAME} — nothing to pull files into."; return 1; } ;
|
|
127
|
+
echo "";
|
|
128
|
+
echo "rsync ${acct}:${remote}/sites/default/files → ${docroot}/sites/default/files (adds and overwrites, never deletes).";
|
|
129
|
+
mkdir -p "${project_path}/${docroot}/sites/default/files" ;
|
|
130
|
+
rsync -az --info=progress2 -e "ssh -i $(cpanel_ssh_key) -o BatchMode=yes" "${acct}:${remote}/sites/default/files/" "${project_path}/${docroot}/sites/default/files/" || return 1 ;
|
|
131
|
+
echo " If Drupal still shows broken images: ddev drush cr";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cpanel_do_push() {
|
|
135
|
+
local project_path="$1" acct="$2" env="$3" docroot remote a;
|
|
136
|
+
remote="$(cpanel_link_get "${project_path}" remote)";
|
|
137
|
+
docroot="$(cpanel_docroot "${project_path}")";
|
|
138
|
+
[ -z "${docroot}" ] && { echo "No web/ or docroot/ in ${doc_name}/${PROJECT_NAME} — nothing to deploy."; return 1; } ;
|
|
139
|
+
echo "Deploy ${docroot}/ of ${doc_name}/${PROJECT_NAME} → ${acct}:${remote}/ (${env})";
|
|
140
|
+
echo " rsync --delete, excluding sites/default/files, sites/*/settings*.php, .git, .ddev. The live site changes.";
|
|
141
|
+
read -r -p " Continue? [y/N] " a < /dev/tty || return 1 ;
|
|
142
|
+
[[ "${a}" =~ ^[Yy] ]] || { echo "Not deployed."; return 1; } ;
|
|
143
|
+
rsync -az --delete --info=progress2 -e "ssh -i $(cpanel_ssh_key) -o BatchMode=yes" \
|
|
144
|
+
--exclude 'sites/default/files' --exclude 'sites/*/settings*.php' --exclude '.git' --exclude '.ddev' \
|
|
145
|
+
"${project_path}/${docroot}/" "${acct}:${remote}/" || return 1 ;
|
|
146
|
+
if [ -d "${project_path}/vendor" ] ; then
|
|
147
|
+
echo " vendor/ is beside the docroot locally: syncing it to $(dirname "${remote}")/vendor/ too.";
|
|
148
|
+
rsync -az --delete -e "ssh -i $(cpanel_ssh_key) -o BatchMode=yes" "${project_path}/vendor/" "${acct}:$(dirname "${remote}")/vendor/" || return 1 ;
|
|
149
|
+
fi
|
|
150
|
+
echo " Deployed. Clear caches on the server: ssh ${acct} 'cd ${remote} && drush cr'";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cpanel_do_logs() {
|
|
154
|
+
local project_path="$1" remote;
|
|
155
|
+
remote="$(cpanel_link_get "${project_path}" remote)";
|
|
156
|
+
cpanel_ssh "${project_path}" "for f in '${remote}/error_log' \"\$HOME/logs/\"*error_log; do [ -f \"\$f\" ] && { echo \"— \$f —\"; tail -50 \"\$f\"; }; done" ;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cpanel_status() {
|
|
160
|
+
local project_path="$1" acct="$2";
|
|
161
|
+
if [ -z "${acct}" ] ; then
|
|
162
|
+
echo " (the link is user@host:/remote/docroot)";
|
|
163
|
+
return 0 ;
|
|
164
|
+
fi
|
|
165
|
+
echo " remote: $(cpanel_link_get "${project_path}" remote)";
|
|
166
|
+
echo "";
|
|
167
|
+
cpanel_do_environments "${project_path}" ;
|
|
168
|
+
echo "";
|
|
169
|
+
echo " Console: https://$(cpanel_link_get "${project_path}" host):2083/";
|
|
170
|
+
echo " Data: --pull-db and --pull-files bring the site down into DDEV over SSH; --push deploys with rsync.";
|
|
171
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
## The host side of the cPanel plugin: the CLI on this machine and the dashboard's credentials.
|
|
3
|
+
## Run by core/scripts/plugins.sh --install cpanel, and by core/scripts/install.sh for every installed
|
|
4
|
+
## plugin that ships one. Idempotent: whatever is already there is reported, not redone. Offered,
|
|
5
|
+
## never automatic: a hosting CLI carries cloud credentials for real environments.
|
|
6
|
+
set -u
|
|
7
|
+
WORKSPACE_ROOT="${WORKSPACE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." && pwd)}";
|
|
8
|
+
|
|
9
|
+
STATE="$(grep -E '^api_tokens:' "${WORKSPACE_ROOT}/core/config/plugin.cpanel.settings.yml" 2>/dev/null | head -1 | sed -E 's/^[a-z_]+:[[:space:]]*//')";
|
|
10
|
+
case "${STATE}" in
|
|
11
|
+
''|CHANGE_ME|"''"|'""') printf ' ✗ %-16s %s\n' "api_tokens" "not set in plugin.cpanel.settings.yml — the dashboard cannot reach cPanel" ;;
|
|
12
|
+
*) printf ' ✓ %-16s %s\n' "api_tokens" "set (plugin.cpanel.settings.yml)" ;;
|
|
13
|
+
esac
|
|
14
|
+
|