@webship.co/workspace-aws 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 CHANGED
@@ -1,3 +1,61 @@
1
- # Temporary Holding Version
1
+ # @webship.co/workspace-aws
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AWS for the [Webship Workspace](https://github.com/webship/workspace) dashboard: the **AWS**
4
+ menu on every site row, a live-site chip on the project card, "Link to AWS" when a new folder is
5
+ made, and the `cmd-tools-aws.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
+ Elastic Beanstalk is the AWS product that has environments the way the family understands them: an application with named environments, each with a URL, a status and a health. A deploy zips the code, uploads it to the Beanstalk bucket, creates an application version and updates the environment. The database (RDS) is not reachable generically from a laptop, so there is no `--pull-db`; the public files come from an S3 bucket named once with `--files-bucket`.
10
+
11
+ ## Use it
12
+
13
+ ```bash
14
+ cd ~/workspace
15
+ bash core/scripts/plugins.sh --require github:webship/workspace-aws
16
+ bash core/scripts/plugins.sh --activate aws
17
+ bash core/scripts/plugins.sh --install aws
18
+ bash core/scripts/plugins.sh --configure aws # core/config/plugin.aws.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-aws.sh <project> [verb]
28
+ ```
29
+
30
+ | Verb | What it does |
31
+ | --- | --- |
32
+ | `--link` <value> | Link the project to an Elastic Beanstalk application by name (aws elasticbeanstalk describe-applications). Add --region to pin its region. |
33
+ | `--region` <value> | The region of the application, remembered by --link. Default: the plugin's setting. |
34
+ | `--files-bucket` <value> | Remember the S3 bucket (and optional prefix: bucket/prefix) --pull-files syncs the public files from. |
35
+ | `--push` | Deploy: zip the current code (git archive), upload it to the Beanstalk storage bucket, create an application version and update the selected environment to it. Outward-facing: asks before updating. |
36
+ | `--environment` <value> | Environment for THIS run: the --push target, the --logs and --pull-files source. Default: the one remembered by --select. |
37
+ | `--environments` | List the application's environments with their status, health and URL. |
38
+ | `--select` <value> | Remember which environment this project works against (in .aws/workspace.yml, git-excluded). Checked against the real list. |
39
+ | `--pull-files` | aws s3 sync the files bucket into the local docroot's sites/default/files (adds and overwrites, never deletes). Client data. |
40
+ | `--logs` | Fetch the selected environment's tail logs (request-environment-info tail, then retrieve). |
41
+ | `--activities` | List the application's last 10 events. |
42
+ | `--force` | Allow --link to relink a project that already points at a different application. |
43
+
44
+ The link lives in the project's `.aws/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, awsProvider(api))` |
52
+ | `src/lib/provider.ts` | The AWS 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-aws.sh` | The per-workspace command |
55
+ | `scripts/functions/fun-aws.sh` | The provider's bash hooks (aws) |
56
+ | `scripts/functions/fun-aws-tool.sh` | The family's dispatcher: link file, environment selection, safety export, status |
57
+ | `scripts/args/arg-aws.sh` | Its argparse |
58
+ | `scripts/host-install.sh` | The host side: the CLI and the credential check |
59
+ | `config/plugin.aws.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 AWS plugin (plugins.sh --install aws); __WORKSPACE__ is the workspace it was installed into.
4
+ #
5
+ # The local ⇄ cloud bridge for one project on AWS: 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 AWS"
17
+ # The args file comes with the plugin: core/scripts/plugins/aws/ is where plugins.sh --install puts it.
18
+ source ${WORKSPACE_SCRIPTS}/plugins/aws/args/arg-aws.sh || exit 1 ;
19
+
20
+ shift $#;
21
+
22
+ aws_project_tool ;
@@ -0,0 +1,11 @@
1
+ # AWS — core/config/plugin.aws.settings.yml
2
+ #
3
+ # The aws CLI inside the dashboard container. Either the host's ~/.aws profile (mounted) or an access key for the dashboard; injected only into AWS jobs, never printed.
4
+ # The named profile in ~/.aws (mounted from the host) the dashboard and the tool use. Ignored when an access key is set below.
5
+ profile: default
6
+ # The region the applications live in, unless the project's link says otherwise.
7
+ region: eu-west-1
8
+ # An IAM access key for the dashboard (optional when the host profile is mounted). Read by the CLI as AWS_ACCESS_KEY_ID.
9
+ access_key_id: CHANGE_ME
10
+ # The secret of that key. Read by the CLI as AWS_SECRET_ACCESS_KEY.
11
+ secret_access_key: CHANGE_ME
@@ -0,0 +1,13 @@
1
+ # Labels for core/config/plugin.aws.settings.yml, merged into the Settings page.
2
+ profile:
3
+ label: AWS profile
4
+ info: 'The named profile in ~/.aws (mounted from the host) the dashboard and the tool use. Ignored when an access key is set below.'
5
+ region:
6
+ label: AWS region
7
+ info: 'The region the applications live in, unless the project''s link says otherwise.'
8
+ access_key_id:
9
+ label: Access key id
10
+ info: 'An IAM access key for the dashboard (optional when the host profile is mounted). Read by the CLI as AWS_ACCESS_KEY_ID.'
11
+ secret_access_key:
12
+ label: Secret access key
13
+ info: 'The secret of that key. Read by the CLI as AWS_SECRET_ACCESS_KEY.'
@@ -0,0 +1,9 @@
1
+ # The aws 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}/.aws:/home/${USER}/.aws:rw"
@@ -0,0 +1,16 @@
1
+ /**
2
+ * AWS for Webship Workspace.
3
+ *
4
+ * The AWS menu on every site row, a live-site chip, the New-folder link option and the
5
+ * `/aws-site/<workspace>/<project>` jump: all of it is the hosting family's surface
6
+ * (@webship.co/workspace-hosting). This package is the AWS 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 { awsProvider } from './lib/provider';
14
+ export { awsProvider };
15
+ export { awsLinkOf } from './lib/link';
16
+ export declare function register(api: PluginApi): void;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * AWS for Webship Workspace.
4
+ *
5
+ * The AWS menu on every site row, a live-site chip, the New-folder link option and the
6
+ * `/aws-site/<workspace>/<project>` jump: all of it is the hosting family's surface
7
+ * (@webship.co/workspace-hosting). This package is the AWS 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.awsLinkOf = exports.awsProvider = 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, "awsProvider", { enumerable: true, get: function () { return provider_1.awsProvider; } });
19
+ var link_1 = require("./lib/link");
20
+ Object.defineProperty(exports, "awsLinkOf", { enumerable: true, get: function () { return link_1.awsLinkOf; } });
21
+ function register(api) {
22
+ (0, workspace_hosting_1.registerHosting)(api, (0, provider_1.awsProvider)(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,6CAA6C;AAEpC,4FAFA,sBAAW,OAEA;AACpB,mCAAuC;AAA9B,iGAAA,SAAS,OAAA;AAElB,SAAgB,QAAQ,CAAC,GAAc;IACrC,IAAA,mCAAe,EAAC,GAAG,EAAE,IAAA,sBAAW,EAAC,GAAG,CAAC,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether a project is linked to AWS, and to what: the `.aws/workspace.yml` file that
3
+ * `cmd-tools-aws.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 AWSLink 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 awsLinkOf(projectDir: string): AWSLink | null;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * Whether a project is linked to AWS, and to what: the `.aws/workspace.yml` file that
4
+ * `cmd-tools-aws.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.awsLinkOf = awsLinkOf;
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, '.aws', '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 awsLinkOf(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,8BASC;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,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,CAAC,CAAC;IAAC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IAClH,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,SAAS,CAAC,UAAkB;IAC1C,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
+ * AWS Elastic Beanstalk, as a hosting provider.
3
+ *
4
+ * The link is `.aws/workspace.yml` (application name, region, files bucket); the environments come
5
+ * from `aws elasticbeanstalk describe-environments` with the host profile or the access key from
6
+ * plugin.aws.settings.yml; the deploy, the logs, the events and the files sync go through
7
+ * cmd-tools-aws.sh in a screen session. No database pull: RDS is not reachable generically.
8
+ */
9
+ import type { PluginApi } from '@webship.co/workspace-plugin-api';
10
+ import type { HostingProvider } from '@webship.co/workspace-hosting';
11
+ export declare function awsProvider(api: PluginApi): HostingProvider;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * AWS Elastic Beanstalk, as a hosting provider.
4
+ *
5
+ * The link is `.aws/workspace.yml` (application name, region, files bucket); the environments come
6
+ * from `aws elasticbeanstalk describe-environments` with the host profile or the access key from
7
+ * plugin.aws.settings.yml; the deploy, the logs, the events and the files sync go through
8
+ * cmd-tools-aws.sh in a screen session. No database pull: RDS is not reachable generically.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.awsProvider = awsProvider;
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const workspace_hosting_1 = require("@webship.co/workspace-hosting");
18
+ const link_1 = require("./link");
19
+ function awsProvider(api) {
20
+ const settings = () => api.settings.read() || {};
21
+ const bin = () => (0, workspace_hosting_1.findCli)('aws') || 'aws';
22
+ // Either the dashboard's own key, or the host's ~/.aws mounted into the container.
23
+ const jobEnv = () => {
24
+ const s = settings();
25
+ const key = (0, workspace_hosting_1.configured)(s.access_key_id);
26
+ const secret = (0, workspace_hosting_1.configured)(s.secret_access_key);
27
+ const base = { ...process.env, AWS_DEFAULT_REGION: String(s.region || 'eu-west-1'), AWS_PAGER: '' };
28
+ if (key && secret)
29
+ return { ...base, AWS_ACCESS_KEY_ID: key, AWS_SECRET_ACCESS_KEY: secret };
30
+ const profile = String(s.profile || 'default');
31
+ const creds = path_1.default.join(process.env.HOME || '', '.aws', 'credentials');
32
+ const config = path_1.default.join(process.env.HOME || '', '.aws', 'config');
33
+ if (fs_1.default.existsSync(creds) || fs_1.default.existsSync(config))
34
+ return { ...base, AWS_PROFILE: profile };
35
+ return null;
36
+ };
37
+ async function environments(link, lib, env) {
38
+ const id = link.id;
39
+ const region = (link.fields && link.fields.region) || env.AWS_DEFAULT_REGION;
40
+ const out = await lib.run(bin(), ['elasticbeanstalk', 'describe-environments', '--application-name', id, '--no-include-deleted', '--region', String(region), '--output', 'json'], process.env.HOME, { timeoutMs: 60 * 1000, env });
41
+ if (!out.ok)
42
+ return { id, rows: [], why: 'list failed' };
43
+ let list = [];
44
+ try {
45
+ list = JSON.parse(String(out.stdout)).Environments || [];
46
+ }
47
+ catch (_) {
48
+ return { id, rows: [], why: 'list failed' };
49
+ }
50
+ const rows = list.map((e) => ({
51
+ title: `${e.EnvironmentName}${e.Health ? ` (${e.Health.toLowerCase()})` : ''}`,
52
+ envId: e.EnvironmentName,
53
+ status: /^ready$/i.test(e.Status) ? 'active' : String(e.Status || 'unknown').toLowerCase(),
54
+ url: e.CNAME ? `http://${e.CNAME}` : (e.EndpointURL ? `http://${e.EndpointURL}` : undefined),
55
+ }));
56
+ return { id, rows };
57
+ }
58
+ return {
59
+ id: 'aws',
60
+ label: 'AWS',
61
+ title: 'AWS',
62
+ cli: 'aws',
63
+ script: 'cmd-tools-aws.sh',
64
+ detect: (projectDir) => (0, link_1.awsLinkOf)(projectDir),
65
+ linkHint: (project) => `bash cmd-tools-aws.sh ${project} --link <beanstalk-application> --region <region>`,
66
+ consoleUrl: (link) => {
67
+ if (!link.id)
68
+ return null;
69
+ const region = (link.fields && link.fields.region) || String(settings().region || 'eu-west-1');
70
+ return `https://${region}.console.aws.amazon.com/elasticbeanstalk/home?region=${region}#/application/overview?applicationName=${encodeURIComponent(link.id)}`;
71
+ },
72
+ jobEnv,
73
+ tokenHint: 'the host ~/.aws profile, or access_key_id and secret_access_key in core/config/plugin.aws.settings.yml',
74
+ environments,
75
+ tools: {
76
+ status: { verb: 'aws', label: 'Link and environments', icon: 'info', args: [] },
77
+ activities: { verb: 'aws-activities', label: 'Last 10 events', icon: 'list', args: ['--activities'] },
78
+ logs: { verb: 'aws-logs', label: 'Tail logs of the environment', icon: 'file-text', args: ['--logs'] },
79
+ deploy: { verb: 'aws-push', label: 'Deploy a version (zip → Beanstalk)…', icon: 'push', args: ['--push'] },
80
+ },
81
+ pull: {
82
+ db: false, files: true,
83
+ dbArgs: () => [],
84
+ filesArgs: (e) => ['--environment', e, '--pull-files'],
85
+ },
86
+ errorHint: (msg) => (/InvalidClientTokenId|SignatureDoesNotMatch|Unable to locate credentials|ExpiredToken/i.test(msg)
87
+ ? `${msg}\n\nThe dashboard reaches AWS with the host's ~/.aws profile (mounted) or access_key_id and secret_access_key in core/config/plugin.aws.settings.yml.`
88
+ : msg),
89
+ folder: {
90
+ idPattern: /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,99}$/,
91
+ idPlaceholder: 'webship-demo',
92
+ idHelp: 'An Elastic Beanstalk application name: letters, digits, spaces, dots, dashes and underscores, up to 100 characters.',
93
+ },
94
+ menuNote: 'RDS is not pulled from here (no --pull-db); files come from the S3 bucket named with --files-bucket',
95
+ };
96
+ }
97
+ //# 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 AWS plugin on a project row
2
+ Once the plugin is activated and installed, every site row carries the AWS menu, and the Plugins
3
+ page lists the plugin with its four steps.
4
+
5
+ Scenario: A site row offers the AWS menu, captioned with its link state
6
+ Given I am on "/dev"
7
+ Then the element ".act-menu-aws" should be displayed
8
+ And I should see "AWS"
9
+ And the element ".act-menu-aws + .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-aws button"
14
+ Then ".uk-dropdown.uk-open" should be visible
15
+ And I should see "Not linked to AWS"
16
+
17
+ Scenario: The plugin is listed on the Plugins page
18
+ Given I am on "/settings/plugins"
19
+ Then I should see "AWS"
20
+ And I should see "@webship.co/workspace-aws"
@@ -0,0 +1,2 @@
1
+ # Directories the dashboard container bind-mounts for this plugin; host-mounts.sh creates them as you first.
2
+ ~/.aws
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
2
  "name": "@webship.co/workspace-aws",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-alpha1",
4
+ "description": "AWS Elastic Beanstalk for Webship Workspace: the AWS menu on every site row, the application's environments and their URLs, deploy a version, tail the logs, recent events, public files from S3. 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-aws.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": "aws",
30
+ "label": "AWS",
31
+ "settingsFile": "plugin.aws.settings.yml",
32
+ "settingsTemplate": "config/plugin.aws.settings.yml",
33
+ "labels": "config/settings.labels.yml",
34
+ "legacySettingsKey": "aws",
35
+ "typeActions": {
36
+ "aws": [
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-aws.sh"
53
+ ]
54
+ },
55
+ "scripts": {
56
+ "functions": [
57
+ "scripts/functions/fun-aws.sh",
58
+ "scripts/functions/fun-aws-tool.sh"
59
+ ],
60
+ "args": [
61
+ "scripts/args/arg-aws.sh"
62
+ ],
63
+ "hostInstall": "scripts/host-install.sh"
64
+ },
65
+ "features": [
66
+ "features/aws.feature"
67
+ ],
68
+ "ddev": {
69
+ "compose": [
70
+ "ddev/docker-compose.aws-cli.yaml"
71
+ ],
72
+ "hostMounts": [
73
+ "~/.aws"
74
+ ]
75
+ }
76
+ },
77
+ "dependencies": {
78
+ "@webship.co/workspace-hosting": "^1.0.0-alpha1"
79
+ },
80
+ "devDependencies": {
81
+ "@types/node": "^24.0.0",
82
+ "@webship.co/workspace-plugin-api": "^1.0.0-alpha1",
83
+ "typescript": "^5.9.0"
84
+ },
85
+ "publishConfig": {
86
+ "access": "public"
87
+ }
88
+ }
@@ -0,0 +1,45 @@
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 Elastic Beanstalk application by name (aws elasticbeanstalk describe-applications). Add --region to pin its region.')
9
+ parser.add_argument('-r', '--region',
10
+ default="_none_",
11
+ help='The region of the application, remembered by --link. Default: the plugin's setting.')
12
+ parser.add_argument('-b', '--files-bucket',
13
+ default="_none_",
14
+ help='Remember the S3 bucket (and optional prefix: bucket/prefix) --pull-files syncs the public files from.')
15
+ parser.add_argument('-p', '--push',
16
+ action='store_true',
17
+ default=False,
18
+ help='Deploy: zip the current code (git archive), upload it to the Beanstalk storage bucket, create an application version and update the selected environment to it. Outward-facing: asks before updating.')
19
+ parser.add_argument('-e', '--environment',
20
+ default="_none_",
21
+ help='Environment for THIS run: the --push target, the --logs and --pull-files source. Default: the one remembered by --select.')
22
+ parser.add_argument('-E', '--environments',
23
+ action='store_true',
24
+ default=False,
25
+ help='List the application's environments with their status, health and URL.')
26
+ parser.add_argument('-s', '--select',
27
+ default="_none_",
28
+ help='Remember which environment this project works against (in .aws/workspace.yml, git-excluded). Checked against the real list.')
29
+ parser.add_argument('-F', '--pull-files',
30
+ action='store_true',
31
+ default=False,
32
+ help='aws s3 sync the files bucket into the local docroot's sites/default/files (adds and overwrites, never deletes). Client data.')
33
+ parser.add_argument('-g', '--logs',
34
+ action='store_true',
35
+ default=False,
36
+ help='Fetch the selected environment's tail logs (request-environment-info tail, then retrieve).')
37
+ parser.add_argument('-a', '--activities',
38
+ action='store_true',
39
+ default=False,
40
+ help='List the application's last 10 events.')
41
+ parser.add_argument('-f', '--force',
42
+ action='store_true',
43
+ default=False,
44
+ help='Allow --link to relink a project that already points at a different application.')
45
+ ARGEOF
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # The AWS tool's dispatcher: generated by the hosting family's scaffolder, the same for every
4
+ # provider. The provider's own hooks live in fun-aws.sh:
5
+ # ensure_aws the CLI and credentials are usable, or say how
6
+ # aws_do_link <project_path> <id> link the project (writes the link file through aws_link_set)
7
+ # aws_list_environments <id> one environment id per line (for --select's check)
8
+ # aws_do_environments <id> print the environments for a person
9
+ # aws_pull_db <project_path> <id> <env> dump the environment's database to stdout path given in $1… (see fun-aws.sh)
10
+ # aws_pull_files <project_path> <id> <env>
11
+ # aws_do_push <project_path> <id> <env> deploy
12
+ # aws_do_logs <project_path> <id> <env>
13
+ # aws_do_activities <project_path> <id>
14
+ # aws_status <project_path> <id> the closing status block
15
+
16
+ AWS_LINK_FILE=".aws/workspace.yml";
17
+
18
+ # One key of the link file, or nothing.
19
+ function aws_link_get() {
20
+ local project_path="$1" key="$2";
21
+ [ -f "${project_path}/${AWS_LINK_FILE}" ] || return 0 ;
22
+ grep -E "^${key}:" "${project_path}/${AWS_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 aws_link_set() {
27
+ local project_path="$1" key="$2" value="$3" file;
28
+ file="${project_path}/${AWS_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 "/.aws/" "${project_path}/.git/info/exclude" 2>/dev/null ; then
37
+ mkdir -p "${project_path}/.git/info" && printf '/.aws/\n' >> "${project_path}/.git/info/exclude" ;
38
+ fi
39
+ }
40
+
41
+ function aws_linked_id() { aws_link_get "$1" id ; }
42
+ function aws_selected_env() { aws_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 aws_target_env() {
48
+ if [ ! "${ENVIRONMENT:-_none_}" == '_none_' ] ; then printf '%s' "${ENVIRONMENT}"; return 0 ; fi
49
+ aws_selected_env "$1";
50
+ }
51
+
52
+ # Where this project serves from: Drupal's two conventions, plus the flat case.
53
+ function aws_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 aws_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 aws_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 aws_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="$(aws_linked_id "${project_path}")" ;
89
+
90
+ if [ ! "${LINK:-_none_}" == '_none_' ] ; then
91
+ ensure_aws || return 1 ;
92
+ if [ -n "${linked_id}" ] && [ ! "${linked_id}" == "${LINK}" ] && [ ! "${FORCE:-no}" == 'yes' ] ; then
93
+ echo "${doc_name}/${PROJECT_NAME} is already linked to AWS ${linked_id}." ;
94
+ echo "Re-run with --force to relink it to ${LINK}." ;
95
+ return 1 ;
96
+ fi
97
+ aws_do_link "${project_path}" "${LINK}" || return 1 ;
98
+ linked_id="$(aws_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 AWS — run with --link <id> first." ;
110
+ return 1 ;
111
+ fi
112
+ ensure_aws || return 1 ;
113
+ fi
114
+
115
+ if [ "${ENVIRONMENTS:-no}" == 'yes' ] ; then
116
+ aws_do_environments "${project_path}" "${linked_id}" || return 1 ;
117
+ echo "";
118
+ echo " Select one: bash cmd-tools-aws.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 ! aws_list_environments "${project_path}" "${linked_id}" 2>/dev/null | grep -qx "${SELECT}" ; then
125
+ echo "AWS ${linked_id} has no environment called '${SELECT}'." ;
126
+ echo " List them: bash cmd-tools-aws.sh ${PROJECT_NAME} --environments" ;
127
+ return 1 ;
128
+ fi
129
+ aws_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="$(aws_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-aws.sh ${PROJECT_NAME} --environments" ;
139
+ echo " bash cmd-tools-aws.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' ] && { aws_pull_db "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
144
+ [ "${PULL_FILES:-no}" == 'yes' ] && { aws_pull_files "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
145
+ [ "${PUSH:-no}" == 'yes' ] && { aws_do_push "${project_path}" "${linked_id}" "${target_env}" || return 1 ; }
146
+ fi
147
+
148
+ if [ "${LOGS:-no}" == 'yes' ] ; then
149
+ aws_do_logs "${project_path}" "${linked_id}" "$(aws_target_env "${project_path}")" || true ;
150
+ fi
151
+ if [ "${ACTIVITIES:-no}" == 'yes' ] ; then
152
+ aws_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 AWS";
159
+ if [ -z "${linked_id}" ]; then
160
+ echo " not linked. Link it with:";
161
+ echo " bash cmd-tools-aws.sh ${PROJECT_NAME} --link <id>";
162
+ aws_status "${project_path}" "" ;
163
+ else
164
+ echo " linked: ${linked_id}";
165
+ local selected;
166
+ selected="$(aws_selected_env "${project_path}")";
167
+ echo " env: ${selected:-none selected — --environments then --select <name>}";
168
+ aws_status "${project_path}" "${linked_id}" ;
169
+ fi
170
+ echo "*---------------------------------------------------------------------*";
171
+
172
+ cd "${WORKSPACE_ROOT}/${doc_name}";
173
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # AWS Elastic Beanstalk through the aws CLI. The hooks the hosting dispatcher (fun-aws-tool.sh)
4
+ # calls for one project directory. Everything defaults to read-only; --link, --push and
5
+ # --pull-files each demand their flag, and nothing here ever creates or terminates an environment.
6
+
7
+ function aws_region() {
8
+ local r; r="$(aws_link_get "$1" region)";
9
+ printf '%s' "${r:-${AWS_DEFAULT_REGION:-${region:-eu-west-1}}}";
10
+ }
11
+
12
+ function ensure_aws() {
13
+ if command -v aws > /dev/null 2>&1 ; then
14
+ if ! aws sts get-caller-identity > /dev/null 2>&1 ; then
15
+ echo "aws is installed but has no usable credentials. On the host: aws configure (or an SSO login); in the dashboard: the plugin settings." ;
16
+ return 1 ;
17
+ fi
18
+ return 0 ;
19
+ fi
20
+ echo "The aws CLI is not installed. Install it first: https://docs.aws.amazon.com/cli/ (the v2 installer, or pipx install awscli)" ;
21
+ return 1 ;
22
+ }
23
+
24
+ function aws_do_link() {
25
+ local project_path="$1" app="$2" r;
26
+ r="${REGION:-_none_}"; [ "${r}" == '_none_' ] && r="$(aws_region "${project_path}")";
27
+ aws elasticbeanstalk describe-applications --application-names "${app}" --region "${r}" --output text --query 'Applications[0].ApplicationName' 2>/dev/null | grep -qx "${app}" \
28
+ || { echo "No Elastic Beanstalk application '${app}' in ${r} (aws elasticbeanstalk describe-applications)."; return 1; } ;
29
+ aws_link_set "${project_path}" id "${app}" ;
30
+ aws_link_set "${project_path}" region "${r}" ;
31
+ if [ ! "${FILES_BUCKET:-_none_}" == '_none_' ] ; then
32
+ aws_link_set "${project_path}" files_bucket "${FILES_BUCKET}" ;
33
+ echo " --pull-files will sync s3://${FILES_BUCKET}." ;
34
+ fi
35
+ }
36
+
37
+ function aws_list_environments() {
38
+ aws elasticbeanstalk describe-environments --application-name "$2" --no-include-deleted --region "$(aws_region "$1")" --output text --query 'Environments[].EnvironmentName' 2>/dev/null | tr '\t' '\n' ;
39
+ }
40
+
41
+ function aws_do_environments() {
42
+ aws elasticbeanstalk describe-environments --application-name "$2" --no-include-deleted --region "$(aws_region "$1")" --output json 2>/dev/null | python3 -c 'import json,sys
43
+ for e in json.load(sys.stdin).get("Environments",[]):
44
+ print(" %-28s %-10s %-8s %-18s %s" % (e.get("EnvironmentName",""), e.get("Status",""), e.get("Health",""), (e.get("VersionLabel") or "")[:18], ("http://"+e["CNAME"]) if e.get("CNAME") else ""))' \
45
+ || { echo " (could not list the environments — check aws sts get-caller-identity)"; return 1; } ;
46
+ }
47
+
48
+ function aws_pull_db() {
49
+ echo "RDS is not reachable generically from this machine: there is no --pull-db here.";
50
+ echo " The shape of it, from a host that can reach the instance: mysqldump -h <rds-endpoint> -u <user> -p <db> | gzip > dump.sql.gz && ddev import-db --file=dump.sql.gz";
51
+ return 1 ;
52
+ }
53
+
54
+ function aws_pull_files() {
55
+ local project_path="$1" app="$2" env="$3" docroot bucket;
56
+ bucket="$(aws_link_get "${project_path}" files_bucket)";
57
+ [ -z "${bucket}" ] && { echo "No files bucket is linked. Name it once: bash cmd-tools-aws.sh ${PROJECT_NAME} --link ${app} --files-bucket <bucket[/prefix]> --force"; return 1; } ;
58
+ docroot="$(aws_docroot "${project_path}")";
59
+ [ -z "${docroot}" ] && { echo "No web/ or docroot/ in ${doc_name}/${PROJECT_NAME} — nothing to pull files into."; return 1; } ;
60
+ echo "";
61
+ echo "aws s3 sync s3://${bucket}/ → ${docroot}/sites/default/files (adds and overwrites, never deletes). Client data.";
62
+ mkdir -p "${project_path}/${docroot}/sites/default/files" ;
63
+ aws s3 sync "s3://${bucket}/" "${project_path}/${docroot}/sites/default/files/" --region "$(aws_region "${project_path}")" || return 1 ;
64
+ echo " If Drupal still shows broken images: ddev drush cr";
65
+ }
66
+
67
+ function aws_do_push() {
68
+ local project_path="$1" app="$2" env="$3" r label zip bucket key a;
69
+ r="$(aws_region "${project_path}")";
70
+ label="$(cd "${project_path}" && git rev-parse --short HEAD 2>/dev/null || date +%s)-$(date +%Y%m%d%H%M%S)";
71
+ echo "Deploy ${doc_name}/${PROJECT_NAME} (${label}) → environment ${env} of application ${app} in ${r}";
72
+ echo " git archive HEAD → zip → the Beanstalk storage bucket → application version → update-environment. The live environment changes.";
73
+ read -r -p " Continue? [y/N] " a < /dev/tty || return 1 ;
74
+ [[ "${a}" =~ ^[Yy] ]] || { echo "Not deployed."; return 1; } ;
75
+ zip="$(mktemp -d)/${PROJECT_NAME}-${label}.zip";
76
+ (cd "${project_path}" && git archive --format=zip -o "${zip}" HEAD) || { echo "git archive failed — the deploy ships the committed tree."; return 1; } ;
77
+ bucket="$(aws elasticbeanstalk create-storage-location --region "${r}" --output text --query S3Bucket)" || return 1 ;
78
+ key="${app}/${PROJECT_NAME}-${label}.zip";
79
+ aws s3 cp "${zip}" "s3://${bucket}/${key}" --region "${r}" || return 1 ;
80
+ aws elasticbeanstalk create-application-version --application-name "${app}" --version-label "${label}" --source-bundle "S3Bucket=${bucket},S3Key=${key}" --region "${r}" > /dev/null || return 1 ;
81
+ aws elasticbeanstalk update-environment --application-name "${app}" --environment-name "${env}" --version-label "${label}" --region "${r}" > /dev/null || return 1 ;
82
+ echo " Version ${label} is deploying to ${env}. Follow it with --activities, or in the console.";
83
+ }
84
+
85
+ function aws_do_logs() {
86
+ local project_path="$1" app="$2" env="$3" r url;
87
+ [ -z "${env}" ] && { echo "Select an environment first (--select) to fetch its logs."; return 1; } ;
88
+ r="$(aws_region "${project_path}")";
89
+ aws elasticbeanstalk request-environment-info --environment-name "${env}" --info-type tail --region "${r}" > /dev/null || return 1 ;
90
+ sleep 8 ;
91
+ url="$(aws elasticbeanstalk retrieve-environment-info --environment-name "${env}" --info-type tail --region "${r}" --output text --query 'EnvironmentInfo[-1].Message')" || return 1 ;
92
+ [ -n "${url}" ] && [ ! "${url}" == 'None' ] && curl -sS "${url}" | tail -200 || echo " (no tail yet — run again in a moment)";
93
+ }
94
+
95
+ function aws_do_activities() {
96
+ aws elasticbeanstalk describe-events --application-name "$2" --max-items 10 --region "$(aws_region "$1")" --output json 2>/dev/null | python3 -c 'import json,sys
97
+ for e in json.load(sys.stdin).get("Events",[])[:10]:
98
+ print(" %-20s %-6s %-24s %s" % ((e.get("EventDate") or "")[:19], e.get("Severity",""), (e.get("EnvironmentName") or "")[:24], e.get("Message","")))' \
99
+ || echo " (could not list the events)" ;
100
+ }
101
+
102
+ function aws_status() {
103
+ local project_path="$1" app="$2";
104
+ if [ -z "${app}" ] ; then
105
+ echo " (find applications with: aws elasticbeanstalk describe-applications)";
106
+ return 0 ;
107
+ fi
108
+ echo " region: $(aws_region "${project_path}")";
109
+ echo " files: $(aws_link_get "${project_path}" files_bucket)";
110
+ echo "";
111
+ aws_do_environments "${project_path}" "${app}" ;
112
+ echo "";
113
+ echo " Console: https://$(aws_region "${project_path}").console.aws.amazon.com/elasticbeanstalk/home#/application/overview?applicationName=${app}";
114
+ echo " --push deploys a version; --pull-files syncs the S3 files bucket; RDS is not pulled from here.";
115
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ ## The host side of the AWS plugin: the CLI on this machine and the dashboard's credentials.
3
+ ## Run by core/scripts/plugins.sh --install aws, 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 aws CLI on this machine, mounted into the dashboard from ~/.local/bin or found on PATH.
10
+ BIN='';
11
+ for candidate in "$(command -v aws 2>/dev/null)" "${HOME}/.local/bin/aws" ; do
12
+ [ -n "${candidate}" ] && [ -x "${candidate}" ] && { BIN="${candidate}"; break; }
13
+ done
14
+ if [ -n "${BIN}" ] ; then
15
+ printf ' ✓ %-16s %s\n' "aws" "${BIN}" ;
16
+ else
17
+ printf ' ✗ %-16s %s\n' "aws" "missing — the AWS menu needs it (docs.aws.amazon.com/cli — the v2 installer, or pipx install awscli)" ;
18
+ fi
19
+
20
+ STATE="$(grep -E '^access_key_id:' "${WORKSPACE_ROOT}/core/config/plugin.aws.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_key_id" "not set in plugin.aws.settings.yml — the dashboard cannot reach AWS" ;;
23
+ *) printf ' ✓ %-16s %s\n' "access_key_id" "set (plugin.aws.settings.yml)" ;;
24
+ esac
25
+
26
+ STATE="$(grep -E '^secret_access_key:' "${WORKSPACE_ROOT}/core/config/plugin.aws.settings.yml" 2>/dev/null | head -1 | sed -E 's/^[a-z_]+:[[:space:]]*//')";
27
+ case "${STATE}" in
28
+ ''|CHANGE_ME|"''"|'""') printf ' ✗ %-16s %s\n' "secret_access_key" "not set in plugin.aws.settings.yml — the dashboard cannot reach AWS" ;;
29
+ *) printf ' ✓ %-16s %s\n' "secret_access_key" "set (plugin.aws.settings.yml)" ;;
30
+ esac
31
+