@webship.co/workspace-digitalocean 0.0.0-stage → 1.0.0-beta1
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 +60 -2
- package/commands/cmd-tools-digitalocean.sh +22 -0
- package/config/plugin.digitalocean.settings.yml +5 -0
- package/config/settings.labels.yml +4 -0
- package/ddev/docker-compose.digitalocean-cli.yaml +9 -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 +11 -0
- package/dist/src/lib/provider.js +77 -0
- package/dist/src/lib/provider.js.map +1 -0
- package/features/digitalocean.feature +20 -0
- package/host-mounts.txt +2 -0
- package/package.json +86 -4
- package/scripts/args/arg-digitalocean.sh +46 -0
- package/scripts/functions/fun-digitalocean-tool.sh +173 -0
- package/scripts/functions/fun-digitalocean.sh +125 -0
- package/scripts/host-install.sh +25 -0
package/README.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @webship.co/workspace-digitalocean
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DigitalOcean for the [Webship Workspace](https://github.com/webship/workspace) dashboard: the **DIGITALOCEAN**
|
|
4
|
+
menu on every site row, a live-site chip on the project card, "Link to DigitalOcean" when a new folder is
|
|
5
|
+
made, and the `cmd-tools-digitalocean.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
|
+
An App Platform app has one environment (production) with one live URL, so the environments list is the app itself and its deployments. A deploy triggers a new deployment from the app's source; the logs are `doctl apps logs`; the database comes from a managed cluster you name once with `--database <cluster-id>` (`doctl databases connection` gives the details, `mysqldump`/`pg_dump` do the dump — your machine's IP must be in the cluster's trusted sources). There is no files mount: App Platform apps keep their files in Spaces, which is an S3 bucket, not something this plugin syncs.
|
|
10
|
+
|
|
11
|
+
## Use it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd ~/workspace
|
|
15
|
+
bash core/scripts/plugins.sh --require github:webship/workspace-digitalocean
|
|
16
|
+
bash core/scripts/plugins.sh --activate digitalocean
|
|
17
|
+
bash core/scripts/plugins.sh --install digitalocean
|
|
18
|
+
bash core/scripts/plugins.sh --configure digitalocean # core/config/plugin.digitalocean.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-digitalocean.sh <project> [verb]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Verb | What it does |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `--link` <value> | Link the project to an App Platform app by its id. Find ids with: doctl apps list |
|
|
33
|
+
| `--database` <value> | Remember the managed database cluster id --pull-db dumps from (doctl databases list). The app's own spec rarely says it. |
|
|
34
|
+
| `--push` | Deploy: trigger a new deployment of the app from its source (doctl apps create-deployment --wait). |
|
|
35
|
+
| `--environment` <value> | Environment for THIS run. An App Platform app has one: production. Kept for the family's shape. |
|
|
36
|
+
| `--environments` | Show the app, its live URL, its active deployment and region. |
|
|
37
|
+
| `--select` <value> | Remember which environment this project works against (production). Checked against the real list. |
|
|
38
|
+
| `--pull-db` | Dump the linked managed database (mysqldump or pg_dump over its connection details) and import it into DDEV. REPLACES the local database — the current one is exported to backups/ first. Client data. |
|
|
39
|
+
| `--logs` | Show the latest deploy log, then follow the run logs (doctl apps logs). |
|
|
40
|
+
| `--activities` | List the last 10 deployments of the app. |
|
|
41
|
+
| `--no-safety-backup` | Skip exporting the local database before --pull-db replaces it. |
|
|
42
|
+
| `--force` | Allow --link to relink a project that already points at a different app. |
|
|
43
|
+
|
|
44
|
+
The link lives in the project's `.digitalocean/workspace.yml` (git-excluded); the dashboard reads it to know
|
|
45
|
+
the project is linked and which environment it works against.
|
|
46
|
+
|
|
47
|
+
## What the package holds
|
|
48
|
+
|
|
49
|
+
| Path | What it is |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `src/index.ts` | `register(api)`: `registerHosting(api, digitaloceanProvider(api))` |
|
|
52
|
+
| `src/lib/provider.ts` | The DigitalOcean provider: the credentials, the environments and their URLs, the verbs, the pulls, the console |
|
|
53
|
+
| `src/lib/link.ts` | The link file reader |
|
|
54
|
+
| `commands/cmd-tools-digitalocean.sh` | The per-workspace command |
|
|
55
|
+
| `scripts/functions/fun-digitalocean.sh` | The provider's bash hooks (doctl) |
|
|
56
|
+
| `scripts/functions/fun-digitalocean-tool.sh` | The family's dispatcher: link file, environment selection, safety export, status |
|
|
57
|
+
| `scripts/args/arg-digitalocean.sh` | Its argparse |
|
|
58
|
+
| `scripts/host-install.sh` | The host side: the CLI and the credential check |
|
|
59
|
+
| `config/plugin.digitalocean.settings.yml` | The settings template |
|
|
60
|
+
|
|
61
|
+
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 DigitalOcean plugin (plugins.sh --install digitalocean); __WORKSPACE__ is the workspace it was installed into.
|
|
4
|
+
#
|
|
5
|
+
# The local ⇄ cloud bridge for one project on DigitalOcean: 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 DigitalOcean"
|
|
17
|
+
# The args file comes with the plugin: core/scripts/plugins/digitalocean/ is where plugins.sh --install puts it.
|
|
18
|
+
source ${WORKSPACE_SCRIPTS}/plugins/digitalocean/args/arg-digitalocean.sh || exit 1 ;
|
|
19
|
+
|
|
20
|
+
shift $#;
|
|
21
|
+
|
|
22
|
+
digitalocean_project_tool ;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# DigitalOcean — core/config/plugin.digitalocean.settings.yml
|
|
2
|
+
#
|
|
3
|
+
# The doctl CLI inside the dashboard container. A personal access token from cloud.digitalocean.com → API → Tokens (read scope for the lists, write for deploys); injected only into DigitalOcean jobs, never printed.
|
|
4
|
+
# A personal access token from cloud.digitalocean.com → API. Read by doctl as DIGITALOCEAN_ACCESS_TOKEN.
|
|
5
|
+
access_token: CHANGE_ME
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The doctl CLI and its session, shared FROM THE HOST: one login, token refreshes land on
|
|
2
|
+
# the host, rebuilds keep it. The bin DIRECTORY is mounted, not the file, so an update on the host
|
|
3
|
+
# is live in the container at once. A machine without the CLI mounts an empty dir and the actions
|
|
4
|
+
# say so.
|
|
5
|
+
services:
|
|
6
|
+
web:
|
|
7
|
+
volumes:
|
|
8
|
+
- "${HOME}/.local/bin:/host-local-bin:ro"
|
|
9
|
+
- "${HOME}/.config/doctl:/home/${USER}/.config/doctl:rw"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigitalOcean for Webship Workspace.
|
|
3
|
+
*
|
|
4
|
+
* The DIGITALOCEAN menu on every site row, a live-site chip, the New-folder link option and the
|
|
5
|
+
* `/digitalocean-site/<workspace>/<project>` jump: all of it is the hosting family's surface
|
|
6
|
+
* (@webship.co/workspace-hosting). This package is the DigitalOcean 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 { digitaloceanProvider } from './lib/provider';
|
|
14
|
+
export { digitaloceanProvider };
|
|
15
|
+
export { digitaloceanLinkOf } from './lib/link';
|
|
16
|
+
export declare function register(api: PluginApi): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DigitalOcean for Webship Workspace.
|
|
4
|
+
*
|
|
5
|
+
* The DIGITALOCEAN menu on every site row, a live-site chip, the New-folder link option and the
|
|
6
|
+
* `/digitalocean-site/<workspace>/<project>` jump: all of it is the hosting family's surface
|
|
7
|
+
* (@webship.co/workspace-hosting). This package is the DigitalOcean 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.digitaloceanLinkOf = exports.digitaloceanProvider = 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, "digitaloceanProvider", { enumerable: true, get: function () { return provider_1.digitaloceanProvider; } });
|
|
19
|
+
var link_1 = require("./lib/link");
|
|
20
|
+
Object.defineProperty(exports, "digitaloceanLinkOf", { enumerable: true, get: function () { return link_1.digitaloceanLinkOf; } });
|
|
21
|
+
function register(api) {
|
|
22
|
+
(0, workspace_hosting_1.registerHosting)(api, (0, provider_1.digitaloceanProvider)(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,6CAAsD;AAE7C,qGAFA,+BAAoB,OAEA;AAC7B,mCAAgD;AAAvC,0GAAA,kBAAkB,OAAA;AAE3B,SAAgB,QAAQ,CAAC,GAAc;IACrC,IAAA,mCAAe,EAAC,GAAG,EAAE,IAAA,+BAAoB,EAAC,GAAG,CAAC,CAAC,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a project is linked to DigitalOcean, and to what: the `.digitalocean/workspace.yml` file that
|
|
3
|
+
* `cmd-tools-digitalocean.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 DigitalOceanLink 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 digitaloceanLinkOf(projectDir: string): DigitalOceanLink | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Whether a project is linked to DigitalOcean, and to what: the `.digitalocean/workspace.yml` file that
|
|
4
|
+
* `cmd-tools-digitalocean.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.digitaloceanLinkOf = digitaloceanLinkOf;
|
|
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, '.digitalocean', '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 digitaloceanLinkOf(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,gDASC;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,eAAe,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC;IAAC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IAC3H,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,kBAAkB,CAAC,UAAkB;IACnD,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,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigitalOcean App Platform, as a hosting provider.
|
|
3
|
+
*
|
|
4
|
+
* The link is `.digitalocean/workspace.yml` (the app id, and the managed database cluster id when
|
|
5
|
+
* one was named); the app's live URL and deployments come from doctl with the access token from
|
|
6
|
+
* plugin.digitalocean.settings.yml injected per call; the deploy, the logs, the deployments list
|
|
7
|
+
* and the database pull go through cmd-tools-digitalocean.sh in a screen session.
|
|
8
|
+
*/
|
|
9
|
+
import type { PluginApi } from '@webship.co/workspace-plugin-api';
|
|
10
|
+
import type { HostingProvider } from '@webship.co/workspace-hosting';
|
|
11
|
+
export declare function digitaloceanProvider(api: PluginApi): HostingProvider;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DigitalOcean App Platform, as a hosting provider.
|
|
4
|
+
*
|
|
5
|
+
* The link is `.digitalocean/workspace.yml` (the app id, and the managed database cluster id when
|
|
6
|
+
* one was named); the app's live URL and deployments come from doctl with the access token from
|
|
7
|
+
* plugin.digitalocean.settings.yml injected per call; the deploy, the logs, the deployments list
|
|
8
|
+
* and the database pull go through cmd-tools-digitalocean.sh in a screen session.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.digitaloceanProvider = digitaloceanProvider;
|
|
12
|
+
const workspace_hosting_1 = require("@webship.co/workspace-hosting");
|
|
13
|
+
const link_1 = require("./link");
|
|
14
|
+
function digitaloceanProvider(api) {
|
|
15
|
+
const settings = () => api.settings.read() || {};
|
|
16
|
+
const bin = () => (0, workspace_hosting_1.findCli)('doctl') || 'doctl';
|
|
17
|
+
const jobEnv = () => {
|
|
18
|
+
const t = (0, workspace_hosting_1.configured)(settings().access_token);
|
|
19
|
+
return t ? { ...process.env, DIGITALOCEAN_ACCESS_TOKEN: t } : null;
|
|
20
|
+
};
|
|
21
|
+
async function environments(link, lib, env) {
|
|
22
|
+
const id = link.id;
|
|
23
|
+
const out = await lib.run(bin(), ['apps', 'get', id, '-o', 'json'], process.env.HOME, { timeoutMs: 60 * 1000, env });
|
|
24
|
+
if (!out.ok)
|
|
25
|
+
return { id, rows: [], why: 'list failed' };
|
|
26
|
+
let apps = [];
|
|
27
|
+
try {
|
|
28
|
+
apps = JSON.parse(String(out.stdout));
|
|
29
|
+
}
|
|
30
|
+
catch (_) {
|
|
31
|
+
return { id, rows: [], why: 'list failed' };
|
|
32
|
+
}
|
|
33
|
+
const app = apps[0];
|
|
34
|
+
if (!app)
|
|
35
|
+
return { id, rows: [], why: 'no such app' };
|
|
36
|
+
const url = app.live_url || app.default_ingress;
|
|
37
|
+
const phase = (app.active_deployment && app.active_deployment.phase) || '';
|
|
38
|
+
// One environment per app: production. A deploying app is still live on its last deployment.
|
|
39
|
+
const status = url && /^active$/i.test(phase) ? 'active' : (app.in_progress_deployment ? 'deploying' : (phase || 'unknown').toLowerCase());
|
|
40
|
+
return { id, rows: [{ title: (app.spec && app.spec.name) || 'production', envId: 'production', status, url: url || undefined }] };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
id: 'digitalocean',
|
|
44
|
+
label: 'DIGITALOCEAN',
|
|
45
|
+
title: 'DigitalOcean',
|
|
46
|
+
cli: 'doctl',
|
|
47
|
+
script: 'cmd-tools-digitalocean.sh',
|
|
48
|
+
detect: (projectDir) => (0, link_1.digitaloceanLinkOf)(projectDir),
|
|
49
|
+
linkHint: (project) => `bash cmd-tools-digitalocean.sh ${project} --link <app-id>`,
|
|
50
|
+
consoleUrl: (link) => (link.id ? `https://cloud.digitalocean.com/apps/${encodeURIComponent(link.id)}` : null),
|
|
51
|
+
jobEnv,
|
|
52
|
+
tokenHint: 'access_token in core/config/plugin.digitalocean.settings.yml',
|
|
53
|
+
environments,
|
|
54
|
+
tools: {
|
|
55
|
+
status: { verb: 'digitalocean', label: 'Link and the app', icon: 'info', args: [] },
|
|
56
|
+
activities: { verb: 'digitalocean-activities', label: 'Last 10 deployments', icon: 'list', args: ['--activities'] },
|
|
57
|
+
logs: { verb: 'digitalocean-logs', label: 'Deploy and run logs', icon: 'file-text', args: ['--logs'] },
|
|
58
|
+
deploy: { verb: 'digitalocean-push', label: 'Deploy (new deployment from source)…', icon: 'push', args: ['--push'] },
|
|
59
|
+
},
|
|
60
|
+
pull: {
|
|
61
|
+
db: true, files: false,
|
|
62
|
+
dbArgs: (e) => ['--environment', e, '--pull-db'],
|
|
63
|
+
filesArgs: () => [],
|
|
64
|
+
noSafetyBackupArg: '--no-safety-backup',
|
|
65
|
+
},
|
|
66
|
+
errorHint: (msg) => (/401|unauthorized|unable to authenticate/i.test(msg)
|
|
67
|
+
? `${msg}\n\nThe dashboard reaches DigitalOcean with access_token in core/config/plugin.digitalocean.settings.yml: a personal access token from cloud.digitalocean.com → API.`
|
|
68
|
+
: msg),
|
|
69
|
+
folder: {
|
|
70
|
+
idPattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
71
|
+
idPlaceholder: 'b7e93f4a-1c2d-4e5f-8a9b-0c1d2e3f4a5b',
|
|
72
|
+
idHelp: 'A DigitalOcean app id is a UUID (doctl apps list, or the id in the app\'s console URL).',
|
|
73
|
+
},
|
|
74
|
+
menuNote: 'Linking and naming the database cluster stay on the command line; files live in Spaces and are not synced',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# 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 DigitalOcean plugin on a project row
|
|
2
|
+
Once the plugin is activated and installed, every site row carries the DIGITALOCEAN menu, and the Plugins
|
|
3
|
+
page lists the plugin with its four steps.
|
|
4
|
+
|
|
5
|
+
Scenario: A site row offers the DIGITALOCEAN menu, captioned with its link state
|
|
6
|
+
Given I am on "/dev"
|
|
7
|
+
Then the element ".act-menu-digitalocean" should be displayed
|
|
8
|
+
And I should see "DIGITALOCEAN"
|
|
9
|
+
And the element ".act-menu-digitalocean + .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-digitalocean button"
|
|
14
|
+
Then ".uk-dropdown.uk-open" should be visible
|
|
15
|
+
And I should see "Not linked to DigitalOcean"
|
|
16
|
+
|
|
17
|
+
Scenario: The plugin is listed on the Plugins page
|
|
18
|
+
Given I am on "/settings/plugins"
|
|
19
|
+
Then I should see "DigitalOcean"
|
|
20
|
+
And I should see "@webship.co/workspace-digitalocean"
|
package/host-mounts.txt
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship.co/workspace-digitalocean",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-beta1",
|
|
4
|
+
"description": "DigitalOcean App Platform for Webship Workspace: the DIGITALOCEAN menu on every site row, the app's live URL and deployments, deploy, logs, a managed database pulled into DDEV. 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-digitalocean.git"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/src",
|
|
14
|
+
"commands",
|
|
15
|
+
"config",
|
|
16
|
+
"features",
|
|
17
|
+
"scripts",
|
|
18
|
+
"README.md",
|
|
19
|
+
"ddev",
|
|
20
|
+
"host-mounts.txt"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"prepare": "npm run build",
|
|
25
|
+
"test": "npm run build && node --test dist/tests/*.test.js",
|
|
26
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"workspacePlugin": {
|
|
29
|
+
"name": "digitalocean",
|
|
30
|
+
"label": "DigitalOcean",
|
|
31
|
+
"settingsFile": "plugin.digitalocean.settings.yml",
|
|
32
|
+
"settingsTemplate": "config/plugin.digitalocean.settings.yml",
|
|
33
|
+
"labels": "config/settings.labels.yml",
|
|
34
|
+
"legacySettingsKey": "digitalocean",
|
|
35
|
+
"typeActions": {
|
|
36
|
+
"digitalocean": [
|
|
37
|
+
"sites"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"commands": {
|
|
41
|
+
"types": [
|
|
42
|
+
"sites"
|
|
43
|
+
],
|
|
44
|
+
"workspaces": [
|
|
45
|
+
"dev",
|
|
46
|
+
"test",
|
|
47
|
+
"demos",
|
|
48
|
+
"sandboxes",
|
|
49
|
+
"projects"
|
|
50
|
+
],
|
|
51
|
+
"files": [
|
|
52
|
+
"commands/cmd-tools-digitalocean.sh"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"functions": [
|
|
57
|
+
"scripts/functions/fun-digitalocean.sh",
|
|
58
|
+
"scripts/functions/fun-digitalocean-tool.sh"
|
|
59
|
+
],
|
|
60
|
+
"args": [
|
|
61
|
+
"scripts/args/arg-digitalocean.sh"
|
|
62
|
+
],
|
|
63
|
+
"hostInstall": "scripts/host-install.sh"
|
|
64
|
+
},
|
|
65
|
+
"features": [
|
|
66
|
+
"features/digitalocean.feature"
|
|
67
|
+
],
|
|
68
|
+
"ddev": {
|
|
69
|
+
"compose": [
|
|
70
|
+
"ddev/docker-compose.digitalocean-cli.yaml"
|
|
71
|
+
],
|
|
72
|
+
"hostMounts": [
|
|
73
|
+
"~/.config/doctl"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@webship.co/workspace-hosting": "^1.0.0-beta1"
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@types/node": "^24.0.0",
|
|
82
|
+
"@webship.co/workspace-plugin-api": "^1.0.0-beta1",
|
|
83
|
+
"typescript": "^5.9.0"
|
|
84
|
+
},
|
|
85
|
+
"publishConfig": {
|
|
86
|
+
"access": "public"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 an App Platform app by its id. Find ids with: doctl apps list')
|
|
9
|
+
parser.add_argument('-d', '--database',
|
|
10
|
+
default="_none_",
|
|
11
|
+
help='Remember the managed database cluster id --pull-db dumps from (doctl databases list). The app's own spec rarely says it.')
|
|
12
|
+
parser.add_argument('-p', '--push',
|
|
13
|
+
action='store_true',
|
|
14
|
+
default=False,
|
|
15
|
+
help='Deploy: trigger a new deployment of the app from its source (doctl apps create-deployment --wait).')
|
|
16
|
+
parser.add_argument('-e', '--environment',
|
|
17
|
+
default="_none_",
|
|
18
|
+
help='Environment for THIS run. An App Platform app has one: production. Kept for the family's shape.')
|
|
19
|
+
parser.add_argument('-E', '--environments',
|
|
20
|
+
action='store_true',
|
|
21
|
+
default=False,
|
|
22
|
+
help='Show the app, its live URL, its active deployment and region.')
|
|
23
|
+
parser.add_argument('-s', '--select',
|
|
24
|
+
default="_none_",
|
|
25
|
+
help='Remember which environment this project works against (production). Checked against the real list.')
|
|
26
|
+
parser.add_argument('-D', '--pull-db',
|
|
27
|
+
action='store_true',
|
|
28
|
+
default=False,
|
|
29
|
+
help='Dump the linked managed database (mysqldump or pg_dump over its connection details) and import it into DDEV. REPLACES the local database — the current one is exported to backups/ first. Client data.')
|
|
30
|
+
parser.add_argument('-g', '--logs',
|
|
31
|
+
action='store_true',
|
|
32
|
+
default=False,
|
|
33
|
+
help='Show the latest deploy log, then follow the run logs (doctl apps logs).')
|
|
34
|
+
parser.add_argument('-a', '--activities',
|
|
35
|
+
action='store_true',
|
|
36
|
+
default=False,
|
|
37
|
+
help='List the last 10 deployments of the app.')
|
|
38
|
+
parser.add_argument('-B', '--no-safety-backup',
|
|
39
|
+
action='store_true',
|
|
40
|
+
default=False,
|
|
41
|
+
help='Skip exporting the local database before --pull-db replaces it.')
|
|
42
|
+
parser.add_argument('-f', '--force',
|
|
43
|
+
action='store_true',
|
|
44
|
+
default=False,
|
|
45
|
+
help='Allow --link to relink a project that already points at a different app.')
|
|
46
|
+
ARGEOF
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# The DigitalOcean tool's dispatcher: generated by the hosting family's scaffolder, the same for every
|
|
4
|
+
# provider. The provider's own hooks live in fun-digitalocean.sh:
|
|
5
|
+
# ensure_digitalocean the CLI and credentials are usable, or say how
|
|
6
|
+
# digitalocean_do_link <project_path> <id> link the project (writes the link file through digitalocean_link_set)
|
|
7
|
+
# digitalocean_list_environments <id> one environment id per line (for --select's check)
|
|
8
|
+
# digitalocean_do_environments <id> print the environments for a person
|
|
9
|
+
# digitalocean_pull_db <project_path> <id> <env> dump the environment's database to stdout path given in $1… (see fun-digitalocean.sh)
|
|
10
|
+
# digitalocean_pull_files <project_path> <id> <env>
|
|
11
|
+
# digitalocean_do_push <project_path> <id> <env> deploy
|
|
12
|
+
# digitalocean_do_logs <project_path> <id> <env>
|
|
13
|
+
# digitalocean_do_activities <project_path> <id>
|
|
14
|
+
# digitalocean_status <project_path> <id> the closing status block
|
|
15
|
+
|
|
16
|
+
DIGITALOCEAN_LINK_FILE=".digitalocean/workspace.yml";
|
|
17
|
+
|
|
18
|
+
# One key of the link file, or nothing.
|
|
19
|
+
function digitalocean_link_get() {
|
|
20
|
+
local project_path="$1" key="$2";
|
|
21
|
+
[ -f "${project_path}/${DIGITALOCEAN_LINK_FILE}" ] || return 0 ;
|
|
22
|
+
grep -E "^${key}:" "${project_path}/${DIGITALOCEAN_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 digitalocean_link_set() {
|
|
27
|
+
local project_path="$1" key="$2" value="$3" file;
|
|
28
|
+
file="${project_path}/${DIGITALOCEAN_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 "/.digitalocean/" "${project_path}/.git/info/exclude" 2>/dev/null ; then
|
|
37
|
+
mkdir -p "${project_path}/.git/info" && printf '/.digitalocean/\n' >> "${project_path}/.git/info/exclude" ;
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function digitalocean_linked_id() { digitalocean_link_get "$1" id ; }
|
|
42
|
+
function digitalocean_selected_env() { digitalocean_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 digitalocean_target_env() {
|
|
48
|
+
if [ ! "${ENVIRONMENT:-_none_}" == '_none_' ] ; then printf '%s' "${ENVIRONMENT}"; return 0 ; fi
|
|
49
|
+
digitalocean_selected_env "$1";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Where this project serves from: Drupal's two conventions, plus the flat case.
|
|
53
|
+
function digitalocean_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 digitalocean_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 digitalocean_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 digitalocean_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="$(digitalocean_linked_id "${project_path}")" ;
|
|
89
|
+
|
|
90
|
+
if [ ! "${LINK:-_none_}" == '_none_' ] ; then
|
|
91
|
+
ensure_digitalocean || return 1 ;
|
|
92
|
+
if [ -n "${linked_id}" ] && [ ! "${linked_id}" == "${LINK}" ] && [ ! "${FORCE:-no}" == 'yes' ] ; then
|
|
93
|
+
echo "${doc_name}/${PROJECT_NAME} is already linked to DigitalOcean ${linked_id}." ;
|
|
94
|
+
echo "Re-run with --force to relink it to ${LINK}." ;
|
|
95
|
+
return 1 ;
|
|
96
|
+
fi
|
|
97
|
+
digitalocean_do_link "${project_path}" "${LINK}" || return 1 ;
|
|
98
|
+
linked_id="$(digitalocean_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 DigitalOcean — run with --link <id> first." ;
|
|
110
|
+
return 1 ;
|
|
111
|
+
fi
|
|
112
|
+
ensure_digitalocean || return 1 ;
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [ "${ENVIRONMENTS:-no}" == 'yes' ] ; then
|
|
116
|
+
digitalocean_do_environments "${project_path}" "${linked_id}" || return 1 ;
|
|
117
|
+
echo "";
|
|
118
|
+
echo " Select one: bash cmd-tools-digitalocean.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 ! digitalocean_list_environments "${project_path}" "${linked_id}" 2>/dev/null | grep -qx "${SELECT}" ; then
|
|
125
|
+
echo "DigitalOcean ${linked_id} has no environment called '${SELECT}'." ;
|
|
126
|
+
echo " List them: bash cmd-tools-digitalocean.sh ${PROJECT_NAME} --environments" ;
|
|
127
|
+
return 1 ;
|
|
128
|
+
fi
|
|
129
|
+
digitalocean_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="$(digitalocean_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-digitalocean.sh ${PROJECT_NAME} --environments" ;
|
|
139
|
+
echo " bash cmd-tools-digitalocean.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' ] && { digitalocean_pull_db "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
|
|
144
|
+
[ "${PULL_FILES:-no}" == 'yes' ] && { digitalocean_pull_files "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
|
|
145
|
+
[ "${PUSH:-no}" == 'yes' ] && { digitalocean_do_push "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
if [ "${LOGS:-no}" == 'yes' ] ; then
|
|
149
|
+
digitalocean_do_logs "${project_path}" "${linked_id}" "$(digitalocean_target_env "${project_path}")" || true ;
|
|
150
|
+
fi
|
|
151
|
+
if [ "${ACTIVITIES:-no}" == 'yes' ] ; then
|
|
152
|
+
digitalocean_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 DigitalOcean";
|
|
159
|
+
if [ -z "${linked_id}" ]; then
|
|
160
|
+
echo " not linked. Link it with:";
|
|
161
|
+
echo " bash cmd-tools-digitalocean.sh ${PROJECT_NAME} --link <id>";
|
|
162
|
+
digitalocean_status "${project_path}" "" ;
|
|
163
|
+
else
|
|
164
|
+
echo " linked: ${linked_id}";
|
|
165
|
+
local selected;
|
|
166
|
+
selected="$(digitalocean_selected_env "${project_path}")";
|
|
167
|
+
echo " env: ${selected:-none selected — --environments then --select <name>}";
|
|
168
|
+
digitalocean_status "${project_path}" "${linked_id}" ;
|
|
169
|
+
fi
|
|
170
|
+
echo "*---------------------------------------------------------------------*";
|
|
171
|
+
|
|
172
|
+
cd "${WORKSPACE_ROOT}/${doc_name}";
|
|
173
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# DigitalOcean App Platform (https://cloud.digitalocean.com/apps) through doctl. The hooks the
|
|
4
|
+
# hosting dispatcher (fun-digitalocean-tool.sh) calls for one project directory. Everything
|
|
5
|
+
# defaults to read-only; --link, --push and --pull-db each demand their flag, and nothing here ever
|
|
6
|
+
# creates or deletes an app or a database.
|
|
7
|
+
|
|
8
|
+
function ensure_digitalocean() {
|
|
9
|
+
if command -v doctl > /dev/null 2>&1 ; then
|
|
10
|
+
if ! doctl account get > /dev/null 2>&1 ; then
|
|
11
|
+
echo "doctl is installed but has no usable token. On the host: doctl auth init (a personal access token from cloud.digitalocean.com → API)." ;
|
|
12
|
+
return 1 ;
|
|
13
|
+
fi
|
|
14
|
+
return 0 ;
|
|
15
|
+
fi
|
|
16
|
+
echo "doctl is not installed. Install it first:" ;
|
|
17
|
+
echo " https://docs.digitalocean.com/reference/doctl/how-to/install/ (snap install doctl, or the release tarball into ~/.local/bin)" ;
|
|
18
|
+
echo "then authenticate with: doctl auth init" ;
|
|
19
|
+
return 1 ;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function digitalocean_do_link() {
|
|
23
|
+
local project_path="$1" app="$2";
|
|
24
|
+
doctl apps get "${app}" > /dev/null || { echo "No app ${app} on this account (doctl apps list)."; return 1; } ;
|
|
25
|
+
digitalocean_link_set "${project_path}" id "${app}" ;
|
|
26
|
+
if [ ! "${DATABASE:-_none_}" == '_none_' ] ; then
|
|
27
|
+
digitalocean_link_set "${project_path}" database "${DATABASE}" ;
|
|
28
|
+
echo " --pull-db will dump managed database cluster ${DATABASE}." ;
|
|
29
|
+
fi
|
|
30
|
+
# One environment, always: production. Selected now so the pulls take no flag.
|
|
31
|
+
digitalocean_link_set "${project_path}" env production ;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function digitalocean_list_environments() { echo production ; }
|
|
35
|
+
|
|
36
|
+
function digitalocean_do_environments() {
|
|
37
|
+
local app="$2";
|
|
38
|
+
doctl apps get "${app}" -o json | python3 -c 'import json,sys
|
|
39
|
+
for a in json.load(sys.stdin):
|
|
40
|
+
s=a.get("spec",{}); d=a.get("active_deployment",{}) or {}
|
|
41
|
+
print(" %-12s %-28s %-10s %s" % ("production", (s.get("name") or "")[:28], (d.get("phase") or "").lower(), a.get("live_url") or a.get("default_ingress") or ""))
|
|
42
|
+
print(" region: %s updated: %s" % (s.get("region",""), (a.get("updated_at") or "")[:19]))' \
|
|
43
|
+
|| { echo " (could not read the app — check doctl account get)"; return 1; } ;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function digitalocean_pull_db() {
|
|
47
|
+
local project_path="$1" app="$2" env="$3" stamp out dump safety cluster;
|
|
48
|
+
cluster="$(digitalocean_link_get "${project_path}" database)";
|
|
49
|
+
if [ -z "${cluster}" ] ; then
|
|
50
|
+
echo "No managed database cluster is linked. Name it once (doctl databases list):" ;
|
|
51
|
+
echo " bash cmd-tools-digitalocean.sh ${PROJECT_NAME} --link ${app} --database <cluster-id> --force" ;
|
|
52
|
+
return 1 ;
|
|
53
|
+
fi
|
|
54
|
+
stamp="$(date +%Y-%m-%d-%H%M%S)";
|
|
55
|
+
out="${backups}/${doc_name}/${PROJECT_NAME}";
|
|
56
|
+
mkdir -p "${out}" ;
|
|
57
|
+
dump="${out}/${PROJECT_NAME}-digitalocean-${env}-${stamp}.sql.gz";
|
|
58
|
+
safety="${out}/${PROJECT_NAME}-before-digitalocean-pull-${stamp}.sql.gz";
|
|
59
|
+
echo "";
|
|
60
|
+
echo "Pulling database cluster ${cluster} of app ${app}.";
|
|
61
|
+
echo " This is client data: it lands on this machine and in ${out}.";
|
|
62
|
+
echo " Your IP must be among the cluster's trusted sources, or the connection is refused.";
|
|
63
|
+
echo "";
|
|
64
|
+
digitalocean_safety_export "${project_path}" "${safety}" ;
|
|
65
|
+
local conn; conn="$(doctl databases connection "${cluster}" -o json)" || return 1 ;
|
|
66
|
+
local engine; engine="$(doctl databases get "${cluster}" -o json | python3 -c 'import json,sys; print(json.load(sys.stdin)[0].get("engine",""))')";
|
|
67
|
+
local host port user pass db;
|
|
68
|
+
read -r host port user pass db <<< "$(printf '%s' "${conn}" | python3 -c 'import json,sys; c=json.load(sys.stdin); print(c["host"], c["port"], c["user"], c["password"], c["database"])')";
|
|
69
|
+
echo "Dumping ${db} (${engine}) from ${host}.";
|
|
70
|
+
case "${engine}" in
|
|
71
|
+
mysql)
|
|
72
|
+
MYSQL_PWD="${pass}" mysqldump --ssl --single-transaction --quick --no-tablespaces -h "${host}" -P "${port}" -u "${user}" "${db}" | gzip > "${dump}" || return 1 ;;
|
|
73
|
+
pg)
|
|
74
|
+
PGPASSWORD="${pass}" pg_dump --no-owner --no-acl -h "${host}" -p "${port}" -U "${user}" "${db}" | gzip > "${dump}" || return 1 ;;
|
|
75
|
+
*)
|
|
76
|
+
echo "Engine ${engine} is not one this tool dumps (mysql, pg)."; return 1 ;;
|
|
77
|
+
esac
|
|
78
|
+
digitalocean_import_dump "${project_path}" "${dump}" || return 1 ;
|
|
79
|
+
echo "";
|
|
80
|
+
echo " Imported ${db} into ${doc_name}/${PROJECT_NAME}.";
|
|
81
|
+
echo " pulled dump: ${dump}";
|
|
82
|
+
echo " local backup: ${safety}";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function digitalocean_pull_files() {
|
|
86
|
+
echo "App Platform apps keep their files in Spaces (an S3 bucket): this tool does not sync them.";
|
|
87
|
+
echo " s3cmd sync s3://<bucket>/<prefix>/ $(digitalocean_docroot "$1")/sites/default/files/ is the shape of it.";
|
|
88
|
+
return 1 ;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function digitalocean_do_push() {
|
|
92
|
+
local project_path="$1" app="$2";
|
|
93
|
+
echo "Creating a new deployment of app ${app} from its source (the branch or image the app spec names)…" ;
|
|
94
|
+
doctl apps create-deployment "${app}" --wait || return 1 ;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function digitalocean_do_logs() {
|
|
98
|
+
local app="$2";
|
|
99
|
+
echo "— the most recent deploy log —";
|
|
100
|
+
doctl apps logs "${app}" --type deploy 2>/dev/null | tail -80 ;
|
|
101
|
+
echo "— run logs (Ctrl-C stops following) —";
|
|
102
|
+
doctl apps logs "${app}" --type run --follow ;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function digitalocean_do_activities() {
|
|
106
|
+
doctl apps list-deployments "$2" -o json 2>/dev/null | python3 -c 'import json,sys
|
|
107
|
+
for d in json.load(sys.stdin)[:10]:
|
|
108
|
+
print(" %-20s %-12s %s" % ((d.get("created_at") or "")[:19], (d.get("phase") or "").lower(), d.get("cause","")))' \
|
|
109
|
+
|| echo " (could not list the deployments)" ;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function digitalocean_status() {
|
|
113
|
+
local project_path="$1" app="$2";
|
|
114
|
+
if [ -z "${app}" ] ; then
|
|
115
|
+
echo " (find the app id with: doctl apps list)";
|
|
116
|
+
return 0 ;
|
|
117
|
+
fi
|
|
118
|
+
echo "";
|
|
119
|
+
digitalocean_do_environments "${project_path}" "${app}" ;
|
|
120
|
+
local cluster; cluster="$(digitalocean_link_get "${project_path}" database)";
|
|
121
|
+
echo " database: ${cluster:-none linked — --link ${app} --database <cluster-id> --force}";
|
|
122
|
+
echo "";
|
|
123
|
+
echo " Console: https://cloud.digitalocean.com/apps/${app}";
|
|
124
|
+
echo " Data: --pull-db brings the managed database down into DDEV; --push triggers a deployment.";
|
|
125
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
## The host side of the DigitalOcean plugin: the CLI on this machine and the dashboard's credentials.
|
|
3
|
+
## Run by core/scripts/plugins.sh --install digitalocean, 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
|
+
# The doctl CLI on this machine, mounted into the dashboard from ~/.local/bin or found on PATH.
|
|
10
|
+
BIN='';
|
|
11
|
+
for candidate in "$(command -v doctl 2>/dev/null)" "${HOME}/.local/bin/doctl" ; do
|
|
12
|
+
[ -n "${candidate}" ] && [ -x "${candidate}" ] && { BIN="${candidate}"; break; }
|
|
13
|
+
done
|
|
14
|
+
if [ -n "${BIN}" ] ; then
|
|
15
|
+
printf ' ✓ %-16s %s\n' "doctl" "${BIN}" ;
|
|
16
|
+
else
|
|
17
|
+
printf ' ✗ %-16s %s\n' "doctl" "missing — the DIGITALOCEAN menu needs it (docs.digitalocean.com/reference/doctl/how-to/install — snap install doctl, or the GitHub release into ~/.local/bin)" ;
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
STATE="$(grep -E '^access_token:' "${WORKSPACE_ROOT}/core/config/plugin.digitalocean.settings.yml" 2>/dev/null | head -1 | sed -E 's/^[a-z_]+:[[:space:]]*//')";
|
|
21
|
+
case "${STATE}" in
|
|
22
|
+
''|CHANGE_ME|"''"|'""') printf ' ✗ %-16s %s\n' "access_token" "not set in plugin.digitalocean.settings.yml — the dashboard cannot reach DigitalOcean" ;;
|
|
23
|
+
*) printf ' ✓ %-16s %s\n' "access_token" "set (plugin.digitalocean.settings.yml)" ;;
|
|
24
|
+
esac
|
|
25
|
+
|