@saws/cli 2.0.0-beta.2 → 2.0.0-beta.21
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 +206 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/saws.d.ts +0 -1
- package/dist/bin/saws.js +32 -40
- package/dist/commands/app/deploy.d.ts +4 -0
- package/dist/commands/app/deploy.js +20 -0
- package/dist/commands/app/files.d.ts +4 -0
- package/dist/commands/app/files.js +40 -0
- package/dist/commands/app/index.d.ts +2 -0
- package/dist/commands/app/index.js +22 -0
- package/dist/commands/app/install.d.ts +4 -0
- package/dist/commands/app/install.js +35 -0
- package/dist/commands/app/list.d.ts +1 -0
- package/dist/commands/app/list.js +67 -0
- package/dist/commands/app/metadata.d.ts +2 -0
- package/dist/commands/app/metadata.js +27 -0
- package/dist/commands/app/paths.d.ts +2 -0
- package/dist/commands/app/paths.js +11 -0
- package/dist/commands/app/process.d.ts +3 -0
- package/dist/commands/app/process.js +53 -0
- package/dist/commands/app/update.d.ts +1 -0
- package/dist/commands/app/update.js +62 -0
- package/dist/commands/deploy/command.d.ts +2 -7
- package/dist/commands/deploy/command.js +13 -19
- package/dist/commands/deploy/index.d.ts +0 -1
- package/dist/commands/deploy/index.js +1 -4
- package/dist/commands/dev/command.d.ts +1 -7
- package/dist/commands/dev/command.js +46 -48
- package/dist/commands/dev/index.d.ts +0 -1
- package/dist/commands/dev/index.js +1 -6
- package/dist/commands/dev/tui/dev-tui.d.ts +18 -3
- package/dist/commands/dev/tui/dev-tui.js +263 -29
- package/dist/commands/host/command.d.ts +3 -2
- package/dist/commands/host/command.js +112 -7
- package/dist/commands/host/create.d.ts +9 -0
- package/dist/commands/host/create.js +30 -0
- package/dist/commands/host/deployment-key.d.ts +2 -0
- package/dist/commands/host/deployment-key.js +40 -0
- package/dist/commands/host/import-key.d.ts +5 -0
- package/dist/commands/host/import-key.js +40 -0
- package/dist/commands/host/index.d.ts +0 -1
- package/dist/commands/host/index.js +25 -3
- package/dist/commands/init/command.d.ts +1 -7
- package/dist/commands/init/command.js +24 -15
- package/dist/commands/init/index.d.ts +0 -1
- package/dist/commands/init/index.js +1 -5
- package/dist/commands/init/templates/gitignore.template.d.ts +1 -0
- package/dist/commands/init/templates/gitignore.template.js +4 -0
- package/dist/commands/init/templates/saws-ts.template.d.ts +3 -0
- package/dist/commands/init/templates/saws-ts.template.js +7 -0
- package/dist/commands/init/templates/tsconfig-json.template.d.ts +1 -0
- package/dist/commands/init/templates/tsconfig-json.template.js +4 -0
- package/dist/commands/logs/command.d.ts +5 -0
- package/dist/commands/logs/command.js +19 -0
- package/dist/commands/logs/index.d.ts +2 -0
- package/dist/commands/logs/index.js +7 -0
- package/dist/commands/secrets/command.d.ts +2 -2
- package/dist/commands/secrets/command.js +31 -9
- package/dist/commands/secrets/index.d.ts +0 -1
- package/dist/commands/secrets/index.js +7 -5
- package/dist/hosts.d.ts +1 -3
- package/dist/hosts.js +11 -10
- package/docs/global-hosts-and-applications.md +307 -0
- package/package.json +13 -20
- package/dist/bin/saws.d.ts.map +0 -1
- package/dist/commands/deploy/command.d.ts.map +0 -1
- package/dist/commands/deploy/index.d.ts.map +0 -1
- package/dist/commands/dev/command.d.ts.map +0 -1
- package/dist/commands/dev/index.d.ts.map +0 -1
- package/dist/commands/dev/tui/dev-tui.d.ts.map +0 -1
- package/dist/commands/host/command.d.ts.map +0 -1
- package/dist/commands/host/command.test.d.ts +0 -2
- package/dist/commands/host/command.test.d.ts.map +0 -1
- package/dist/commands/host/command.test.js +0 -13
- package/dist/commands/host/index.d.ts.map +0 -1
- package/dist/commands/init/command.d.ts.map +0 -1
- package/dist/commands/init/command.test.d.ts +0 -2
- package/dist/commands/init/command.test.d.ts.map +0 -1
- package/dist/commands/init/command.test.js +0 -40
- package/dist/commands/init/index.d.ts.map +0 -1
- package/dist/commands/secrets/command.d.ts.map +0 -1
- package/dist/commands/secrets/index.d.ts.map +0 -1
- package/dist/hosts.d.ts.map +0 -1
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { configureHostCommand } from "./command.js";
|
|
3
|
+
import { createHostCommand } from "./create.js";
|
|
4
|
+
import { importHostKeyCommand } from "./import-key.js";
|
|
3
5
|
export const createCommand = () => new Command("host")
|
|
4
6
|
.description("configure deployment hosts")
|
|
7
|
+
.addCommand(new Command("create")
|
|
8
|
+
.description("create a global deployment host")
|
|
9
|
+
.argument("<name>", "global host name")
|
|
10
|
+
.requiredOption("--address <address>", "host name or IP address")
|
|
11
|
+
.requiredOption("--user <deployment-user>", "persistent deployment account")
|
|
12
|
+
.option("--ssh-port <port>", "SSH port")
|
|
13
|
+
.option("--platform <platform>", 'Docker platform such as "linux/amd64"')
|
|
14
|
+
.option("--exposure <mode>", 'network exposure policy: "tunnel" or "public"')
|
|
15
|
+
.option("--allowed-tcp-port <port...>", "public TCP ports allowed by the host firewall")
|
|
16
|
+
.action(createHostCommand))
|
|
17
|
+
.addCommand(new Command("key")
|
|
18
|
+
.description("manage global host deployment keys")
|
|
19
|
+
.addCommand(new Command("import")
|
|
20
|
+
.description("import an existing private key into encrypted global storage")
|
|
21
|
+
.argument("<name>", "global host name")
|
|
22
|
+
.requiredOption("--file <path>", "path to an SSH private key")
|
|
23
|
+
.option("--force", "replace an existing deployment key")
|
|
24
|
+
.action(importHostKeyCommand)))
|
|
5
25
|
.addCommand(new Command("configure")
|
|
6
|
-
.description("
|
|
26
|
+
.description("bootstrap a deployment user and apply host security policy")
|
|
7
27
|
.argument("[name]", "host name; optional when exactly one host exists")
|
|
8
|
-
.
|
|
9
|
-
.option("--
|
|
28
|
+
.requiredOption("--user <bootstrap-user>", "existing SSH account used for initial configuration")
|
|
29
|
+
.option("--config <path>", "path to saws.ts")
|
|
30
|
+
.option("--global", "configure a host from the global host registry")
|
|
31
|
+
.option("--dry-run", "describe configuration without making changes")
|
|
10
32
|
.action(configureHostCommand));
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
rootDir?: string;
|
|
3
|
-
config?: string;
|
|
4
|
-
dryRun?: boolean;
|
|
5
|
-
}
|
|
6
|
-
export declare function initCommand(serviceName: string, path: string | undefined, options: InitCommandOptions): Promise<void>;
|
|
7
|
-
//# sourceMappingURL=command.d.ts.map
|
|
1
|
+
export declare const initCommand: (serviceName?: string, configPath?: string) => Promise<void>;
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { findServiceDefinition, getSawsConfig } from "@saws/core";
|
|
4
|
+
import { installDependencies } from "@saws/core/utils/dependency-management";
|
|
5
|
+
import { createFileIfNotExists } from "@saws/core/utils/create-file-if-not-exists";
|
|
6
|
+
import { sawsTsTemplate } from "./templates/saws-ts.template.js";
|
|
7
|
+
import { tsconfigJsonTemplate } from "./templates/tsconfig-json.template.js";
|
|
8
|
+
import { gitignoreTemplate } from "./templates/gitignore.template.js";
|
|
9
|
+
export const initCommand = async (serviceName, configPath) => {
|
|
10
|
+
if (serviceName != null) {
|
|
11
|
+
const serviceDefinition = await getSawsConfig(configPath);
|
|
12
|
+
await findServiceDefinition(serviceDefinition, serviceName).init();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const name = path.parse(path.resolve(".")).name;
|
|
16
|
+
// not used for now
|
|
17
|
+
await installDependencies([]);
|
|
18
|
+
await installDependencies(["@saws/core", "typescript", "@tsconfig/node26"], {
|
|
19
|
+
development: true,
|
|
20
|
+
});
|
|
21
|
+
await fs.writeFile("./tsconfig.json", tsconfigJsonTemplate(), {});
|
|
22
|
+
await createFileIfNotExists("./saws.ts", sawsTsTemplate({ name }));
|
|
23
|
+
await createFileIfNotExists("./.gitignore", gitignoreTemplate());
|
|
24
|
+
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { initCommand } from "./command.js";
|
|
3
3
|
export const createCommand = () => new Command("init")
|
|
4
|
-
.
|
|
5
|
-
.argument("<service>", "name of the service to initialize")
|
|
4
|
+
.argument("[service]", "name of the service to initialize")
|
|
6
5
|
.argument("[config]", "path to service definition")
|
|
7
|
-
.option("--root-dir <string>", "project root used for generated files")
|
|
8
|
-
.option("--config <string>", "path to service definition")
|
|
9
|
-
.option("--dry-run", "allow service initializers to preview their changes")
|
|
10
6
|
.action(initCommand);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const gitignoreTemplate: () => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const tsconfigJsonTemplate: () => string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findServiceDefinition, getSawsConfig } from "@saws/core";
|
|
2
|
+
export const logsCommand = async (serviceName, options) => {
|
|
3
|
+
const { config, stage } = options;
|
|
4
|
+
if (stage == null || stage.length === 0) {
|
|
5
|
+
throw new Error("logs requires --stage <string>");
|
|
6
|
+
}
|
|
7
|
+
if (stage === "local")
|
|
8
|
+
return;
|
|
9
|
+
process.env.STAGE = stage;
|
|
10
|
+
const serviceDefinition = await getSawsConfig(config);
|
|
11
|
+
if (serviceName != null) {
|
|
12
|
+
await findServiceDefinition(serviceDefinition, serviceName).logs(stage);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await Promise.all(collectServices(serviceDefinition).map((service) => service.logs(stage)));
|
|
16
|
+
};
|
|
17
|
+
function collectServices(root) {
|
|
18
|
+
return [...new Set(root.getAllDependencies())];
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { logsCommand } from "./command.js";
|
|
3
|
+
export const createCommand = () => new Command("logs")
|
|
4
|
+
.option("--stage <string>", "stage to tail logs from")
|
|
5
|
+
.option("--config <path>", "path to service definition")
|
|
6
|
+
.argument("[service]", "name of the service to tail logs from")
|
|
7
|
+
.action(logsCommand);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export interface SecretsCommandOptions {
|
|
2
|
+
config?: string;
|
|
2
3
|
stage?: string;
|
|
4
|
+
global?: boolean;
|
|
3
5
|
set?: string;
|
|
4
6
|
get?: boolean;
|
|
5
|
-
rootDir?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function secretsCommand(name: string, options: SecretsCommandOptions): Promise<void>;
|
|
8
|
-
//# sourceMappingURL=command.d.ts.map
|
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
import { SecretsManager } from "@saws/
|
|
1
|
+
import { getSawsConfigModule, SecretsManager } from "@saws/core";
|
|
2
2
|
export async function secretsCommand(name, options) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
const value = options.set;
|
|
4
|
+
if (options.get && value != null) {
|
|
5
|
+
throw new Error("secrets accepts only one of --get or --set <value>");
|
|
6
|
+
}
|
|
7
|
+
if (!options.get && value == null) {
|
|
8
|
+
throw new Error("secrets requires either --get or --set <value>");
|
|
9
|
+
}
|
|
10
|
+
if (!options.global) {
|
|
11
|
+
process.env.STAGE = options.stage ?? "local";
|
|
12
|
+
}
|
|
13
|
+
const config = await getSawsConfigModule(options.config);
|
|
14
|
+
const manager = Object.values(config).find((candidate) => candidate instanceof SecretsManager) ?? config.secrets;
|
|
15
|
+
if (!isSecretsManagerLike(manager)) {
|
|
16
|
+
throw new Error('saws.ts must export a SecretsManager instance named "secrets"');
|
|
17
|
+
}
|
|
18
|
+
const secrets = options.global ? manager.global : manager;
|
|
7
19
|
if (options.get) {
|
|
8
|
-
console.log(await
|
|
20
|
+
console.log(await secrets.get(name));
|
|
9
21
|
return;
|
|
10
22
|
}
|
|
11
|
-
if (
|
|
12
|
-
throw new Error("secrets requires
|
|
23
|
+
if (value == null) {
|
|
24
|
+
throw new Error("secrets requires --set <value>");
|
|
13
25
|
}
|
|
14
|
-
await
|
|
26
|
+
await secrets.set(name, value);
|
|
15
27
|
console.log("Set secret");
|
|
16
28
|
}
|
|
29
|
+
function isSecretsManagerLike(value) {
|
|
30
|
+
if (value == null || typeof value !== "object")
|
|
31
|
+
return false;
|
|
32
|
+
const candidate = value;
|
|
33
|
+
return (typeof candidate.get === "function" &&
|
|
34
|
+
typeof candidate.set === "function" &&
|
|
35
|
+
candidate.global != null &&
|
|
36
|
+
typeof candidate.global.get === "function" &&
|
|
37
|
+
typeof candidate.global.set === "function");
|
|
38
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { secretsCommand } from "./command.js";
|
|
3
3
|
export const createCommand = () => new Command("secrets")
|
|
4
|
-
.
|
|
5
|
-
.
|
|
6
|
-
.option("--
|
|
7
|
-
.option("--
|
|
8
|
-
.option("--
|
|
4
|
+
.description("get or set encrypted project secrets")
|
|
5
|
+
.argument("<name>", "secret name")
|
|
6
|
+
.option("--stage <string>", "stage for a stage-scoped secret", "local")
|
|
7
|
+
.option("--global", "use the global secret scope")
|
|
8
|
+
.option("--set <string>", "set the secret value")
|
|
9
|
+
.option("--get", "get the secret value")
|
|
10
|
+
.option("--config <string>", "path to service definition")
|
|
9
11
|
.action(secretsCommand);
|
package/dist/hosts.d.ts
CHANGED
package/dist/hosts.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { Host } from "@saws/
|
|
1
|
+
import { Host } from "@saws/core";
|
|
2
2
|
export function findConfiguredHosts(root) {
|
|
3
3
|
const hosts = new Set();
|
|
4
|
-
const visited = new
|
|
5
|
-
const visit = (
|
|
6
|
-
if (visited.has(
|
|
4
|
+
const visited = new WeakSet();
|
|
5
|
+
const visit = (value) => {
|
|
6
|
+
if (value == null || typeof value !== "object" || visited.has(value))
|
|
7
7
|
return;
|
|
8
|
-
visited.add(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
visited.add(value);
|
|
9
|
+
if (value instanceof Host) {
|
|
10
|
+
hosts.add(value);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const child of Object.values(value))
|
|
14
|
+
visit(child);
|
|
14
15
|
};
|
|
15
16
|
visit(root);
|
|
16
17
|
return [...hosts];
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Global hosts and packaged applications
|
|
2
|
+
|
|
3
|
+
This guide covers the complete remote-deployment workflow:
|
|
4
|
+
|
|
5
|
+
1. Save a machine as a reusable global host.
|
|
6
|
+
2. Bootstrap that machine for SAWS deployments.
|
|
7
|
+
3. Reference the host from an application input file.
|
|
8
|
+
4. Install and deploy a packaged application.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
You need the SAWS CLI, a Debian or Ubuntu server, and an existing SSH account that can run commands
|
|
13
|
+
as `root` or with interactive `sudo`.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install --save-dev @saws/cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
By default, global hosts and installed applications live under `~/.saws`. Set `SAWS_HOME` before
|
|
20
|
+
running any host or application command if that state should live elsewhere:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
export SAWS_HOME=/srv/saws-state
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use the same `SAWS_HOME` for every command that should share hosts, keys, applications, and local
|
|
27
|
+
deployment state.
|
|
28
|
+
|
|
29
|
+
> `saws host configure` applies an opinionated security baseline. It installs and enables Docker,
|
|
30
|
+
> fail2ban, UFW, and unattended upgrades; resets the UFW rules; installs Docker firewall rules;
|
|
31
|
+
> configures a deployment account; and disables SSH password authentication after verifying key
|
|
32
|
+
> access. Use a new server when possible and run the command with `--dry-run` first.
|
|
33
|
+
|
|
34
|
+
## 1. Create a global host
|
|
35
|
+
|
|
36
|
+
A global host is a named machine profile that can be reused by any project or packaged application.
|
|
37
|
+
Create a public host with:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx saws host create edge \
|
|
41
|
+
--address 203.0.113.10 \
|
|
42
|
+
--user saws \
|
|
43
|
+
--platform linux/amd64 \
|
|
44
|
+
--exposure public
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The important values are:
|
|
48
|
+
|
|
49
|
+
- `edge`: the name passed to `getGlobalHost("edge")` later.
|
|
50
|
+
- `--address`: a DNS name or IP address reachable over SSH.
|
|
51
|
+
- `--user`: the persistent, unprivileged account SAWS will create and use for deployments. This is
|
|
52
|
+
not necessarily the account that already exists on the server.
|
|
53
|
+
- `--platform`: the Docker target platform. It is optional, but useful when the development machine
|
|
54
|
+
and server use different CPU architectures. Examples include `linux/amd64` and `linux/arm64`.
|
|
55
|
+
- `--exposure`: `public` or `tunnel`. Public hosts allow selected inbound application ports;
|
|
56
|
+
tunnel hosts allow none.
|
|
57
|
+
|
|
58
|
+
A public host allows TCP ports 80 and 443 by default. Specify the complete public-port set when an
|
|
59
|
+
application needs different ports:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx saws host create observability \
|
|
63
|
+
--address metrics.example.com \
|
|
64
|
+
--user saws \
|
|
65
|
+
--exposure public \
|
|
66
|
+
--allowed-tcp-port 8080 4317 4318
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When `--allowed-tcp-port` is supplied, it replaces the default list; include 80 and 443 explicitly
|
|
70
|
+
if the machine also needs them. The SSH port is allowed separately. To use a nonstandard SSH port,
|
|
71
|
+
add `--ssh-port 2222`.
|
|
72
|
+
|
|
73
|
+
For a private machine reached only through an SSH or network tunnel:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx saws host create internal \
|
|
77
|
+
--address 10.0.0.20 \
|
|
78
|
+
--user saws \
|
|
79
|
+
--exposure tunnel
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Do not supply public ports for a tunnel host.
|
|
83
|
+
|
|
84
|
+
### Where the profile is stored
|
|
85
|
+
|
|
86
|
+
The profile is plain JSON at:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
~/.saws/hosts/<name>/host.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
It contains the address, users, ports, exposure policy, and platform, but no private key. If you edit
|
|
93
|
+
the profile directly, rerun the configure step so the host firewall and readiness record match it.
|
|
94
|
+
Creating a host whose name already exists fails instead of overwriting the profile.
|
|
95
|
+
|
|
96
|
+
## 2. Configure the machine
|
|
97
|
+
|
|
98
|
+
Preview the bootstrap operation first:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
npx saws host configure edge --global --user ubuntu --dry-run
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then apply it:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
npx saws host configure edge --global --user ubuntu
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Here, `ubuntu` is an existing bootstrap account with root or sudo access. It is used only to prepare
|
|
111
|
+
the server. Subsequent deployments connect as the persistent deployment user (`saws` in this
|
|
112
|
+
example) configured by the earlier host-creation command.
|
|
113
|
+
|
|
114
|
+
SAWS generates an Ed25519 deployment key when the global host does not have one, encrypts the
|
|
115
|
+
private key in the global secret store, installs the public key for the deployment account, and only
|
|
116
|
+
then disables password-based SSH access.
|
|
117
|
+
|
|
118
|
+
To use an existing unencrypted, non-passphrase-protected private key, import it before configuring
|
|
119
|
+
the machine:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx saws host key import edge --file ~/.ssh/edge_deploy
|
|
123
|
+
npx saws host configure edge --global --user ubuntu
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The source file is not modified. Import refuses to replace an existing key unless `--force` is
|
|
127
|
+
passed.
|
|
128
|
+
|
|
129
|
+
Global host keys are encrypted beneath `~/.saws/secrets`. The encryption passcode and generated
|
|
130
|
+
public-key metadata are stored in `~/.saws/.env`, with file permissions restricted to the current
|
|
131
|
+
user. Back up both locations securely. Losing the passcode makes the encrypted deployment key
|
|
132
|
+
unusable.
|
|
133
|
+
|
|
134
|
+
## 3. Install an application instance
|
|
135
|
+
|
|
136
|
+
Install the package and give this local instance a name:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
npx saws app install @saws/reverse-proxy-application --name edge-proxy
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The package argument accepts an npm version or tag. Pin a version for the initial installation if
|
|
143
|
+
needed:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
npx saws app install @saws/reverse-proxy-application@2.0.0-beta.17 --name edge-proxy
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
SAWS refuses to install over an existing application name. It creates this local application
|
|
150
|
+
directory:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
~/.saws/apps/edge-proxy/
|
|
154
|
+
├── config.ts
|
|
155
|
+
├── package.json
|
|
156
|
+
├── package-lock.json
|
|
157
|
+
├── saws.ts
|
|
158
|
+
└── node_modules/
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This directory also becomes the home for that instance's secrets, outputs, and other local SAWS
|
|
162
|
+
state. The install command does not deploy anything.
|
|
163
|
+
|
|
164
|
+
List installed application instances, package versions, and configuration state at any time:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
npx saws app list
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 4. Configure the generated file
|
|
171
|
+
|
|
172
|
+
A packaged application exports the shape of the input it expects. Consult that package's README for
|
|
173
|
+
its application-specific fields. Applications that deploy to a remote machine accept or derive a
|
|
174
|
+
`Host`; reference the global profile with `getGlobalHost`.
|
|
175
|
+
|
|
176
|
+
Open the generated configuration in your editor:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
${EDITOR:-vi} "${SAWS_HOME:-$HOME/.saws}/apps/edge-proxy/config.ts"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The generated file derives its type from the installed application's `create` factory. TypeScript
|
|
183
|
+
therefore reports missing or incompatible fields. For example, `config.ts` can route public
|
|
184
|
+
hostnames to ports on the target machine:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { getGlobalHost } from "@saws/core";
|
|
188
|
+
import type { ReverseProxyApplicationConfig } from "@saws/reverse-proxy-application";
|
|
189
|
+
|
|
190
|
+
export default {
|
|
191
|
+
host: getGlobalHost("edge"),
|
|
192
|
+
acmeEmail: "ops@example.com",
|
|
193
|
+
routes: [
|
|
194
|
+
{ hostname: "api.example.com", port: 3000 },
|
|
195
|
+
{ hostname: "app.example.com", port: 8080, healthUri: "/health" },
|
|
196
|
+
],
|
|
197
|
+
} satisfies ReverseProxyApplicationConfig;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
For applications with secrets, export a `SecretsManager` named `secrets` and pass lazy references
|
|
201
|
+
instead of secret values:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { getGlobalHost, SecretsManager } from "@saws/core";
|
|
205
|
+
import type { OpenDesignApplicationConfig } from "@saws/open-design-application";
|
|
206
|
+
|
|
207
|
+
export const secrets = new SecretsManager();
|
|
208
|
+
|
|
209
|
+
export default {
|
|
210
|
+
host: getGlobalHost("edge"),
|
|
211
|
+
registry: "registry.example.com/saws",
|
|
212
|
+
setupPassword: secrets.reference("open-design-application-password"),
|
|
213
|
+
} satisfies OpenDesignApplicationConfig;
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`secrets.reference(...)` is stage-scoped: production and staging can use different values without
|
|
217
|
+
changing the configuration file. Use `secrets.global.reference(...)` only for a value intentionally
|
|
218
|
+
shared across stages.
|
|
219
|
+
|
|
220
|
+
### Update an installed application
|
|
221
|
+
|
|
222
|
+
Update an instance when the version on its npm release channel is newer than its installed semantic
|
|
223
|
+
version:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
npx saws app update edge-proxy
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Update never overwrites `config.ts`. Stable applications follow npm's `latest` dist-tag, while
|
|
230
|
+
applications already on a beta version follow the `beta` dist-tag. If the instance was installed by
|
|
231
|
+
an older SAWS CLI, its first update renames `input.ts` to `config.ts` without changing the contents.
|
|
232
|
+
If both files exist, update stops and asks you to resolve the ambiguity.
|
|
233
|
+
|
|
234
|
+
### Set application secrets
|
|
235
|
+
|
|
236
|
+
Run secret commands from the installed application directory so a default `SecretsManager` resolves
|
|
237
|
+
the correct storage location:
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
cd "${SAWS_HOME:-$HOME/.saws}/apps/edge-proxy"
|
|
241
|
+
npx --package @saws/cli saws secrets example-secret \
|
|
242
|
+
--stage production \
|
|
243
|
+
--set 'replace-with-the-secret-value'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Avoid placing real secret values in shell history when that matters for your environment. The
|
|
247
|
+
encrypted stage file is stored under the installed application's `.saws/secrets` directory.
|
|
248
|
+
|
|
249
|
+
## 5. Deploy the application
|
|
250
|
+
|
|
251
|
+
Deploy a named instance to any non-`local` stage:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
npx saws app deploy edge-proxy --stage production
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The deployment command:
|
|
258
|
+
|
|
259
|
+
1. Loads `~/.saws/apps/edge-proxy/saws.ts`.
|
|
260
|
+
2. Resolves the generated configuration and application factory.
|
|
261
|
+
3. Resolves stage-scoped environment and secrets.
|
|
262
|
+
4. Connects to the global host with its deployment key.
|
|
263
|
+
5. Verifies that the host still matches the configured security policy.
|
|
264
|
+
6. Deploys application dependencies in order and starts or replaces changed containers.
|
|
265
|
+
|
|
266
|
+
By default, stages isolate generated names, Docker networks, persistent volumes, runtime files,
|
|
267
|
+
secrets, and outputs. Deploying `staging` and `production` therefore creates separate application
|
|
268
|
+
resources even when they use the same host. Explicitly overridden volume names can opt out of that
|
|
269
|
+
isolation.
|
|
270
|
+
|
|
271
|
+
For a public application, confirm before deployment that:
|
|
272
|
+
|
|
273
|
+
- DNS records point to the host.
|
|
274
|
+
- Every published TCP port is in the global host's `allowedTcpPorts`.
|
|
275
|
+
- Cloud-provider or upstream-network firewalls allow the same ports.
|
|
276
|
+
- Any required registry credentials and application secrets exist for the target stage.
|
|
277
|
+
- Services addressed by a reverse proxy are reachable from the proxy container.
|
|
278
|
+
|
|
279
|
+
## Operations and troubleshooting
|
|
280
|
+
|
|
281
|
+
To run standard SAWS commands against an installed application, change into its directory:
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
cd "${SAWS_HOME:-$HOME/.saws}/apps/edge-proxy"
|
|
285
|
+
npx --package @saws/cli saws logs --stage production
|
|
286
|
+
npx --package @saws/cli saws logs reverse-proxy --stage production
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Common failures:
|
|
290
|
+
|
|
291
|
+
- **Host is not configured or has drifted:** rerun the global `host configure` command with the host
|
|
292
|
+
name and bootstrap user. This reapplies the declared firewall and security policy.
|
|
293
|
+
- **SSH fails after configuration:** verify that the global key store and `~/.saws/.env` came from
|
|
294
|
+
the same SAWS home and that the configured address and SSH port are correct.
|
|
295
|
+
- **A published service is unreachable:** Docker port publishing and the host allowlist are separate.
|
|
296
|
+
The service must publish the port, and the host profile must allow it.
|
|
297
|
+
- **An HTTPS certificate is not issued:** verify public DNS and inbound ports 80 and 443. A hostname
|
|
298
|
+
route in the reverse-proxy application requests and renews its certificate automatically.
|
|
299
|
+
- **A remote Dockerfile application asks for a registry:** remote hosts cannot use an image that was
|
|
300
|
+
built only on the development machine. Configure the application's `registry` and any required
|
|
301
|
+
registry authentication, or supply a prebuilt image.
|
|
302
|
+
- **A secret cannot be decrypted:** use the same `SAWS_HOME` and `SAWS_SECRETS_PASSCODE` that created
|
|
303
|
+
the encrypted file.
|
|
304
|
+
|
|
305
|
+
Treat `~/.saws` (or the configured `SAWS_HOME`) as operational state. Back it up securely, especially
|
|
306
|
+
the global `.env`, encrypted secret stores, application input files, and any output needed by your
|
|
307
|
+
deployment workflow. Remote Docker volumes must be backed up separately on the target machines.
|
package/package.json
CHANGED
|
@@ -1,34 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saws/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0-beta.21",
|
|
4
|
+
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/shichongrui/saws.git"
|
|
8
|
-
"directory": "packages/cli"
|
|
7
|
+
"url": "git+https://github.com/shichongrui/saws.git"
|
|
9
8
|
},
|
|
10
|
-
"type": "module",
|
|
11
9
|
"bin": {
|
|
12
|
-
"saws": "dist/bin/saws.js"
|
|
10
|
+
"saws": "./dist/bin/saws.js"
|
|
13
11
|
},
|
|
14
12
|
"files": [
|
|
15
|
-
"dist"
|
|
13
|
+
"./dist",
|
|
14
|
+
"./docs"
|
|
16
15
|
],
|
|
17
|
-
"
|
|
18
|
-
"prebuild": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
|
19
|
-
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"postbuild": "chmod +x dist/bin/saws.js",
|
|
21
|
-
"test": "npm run build && node --test dist/commands/**/*.test.js",
|
|
22
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
|
-
},
|
|
16
|
+
"type": "module",
|
|
24
17
|
"dependencies": {
|
|
25
|
-
"@saws/core": "2.0.0-beta.
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
18
|
+
"@saws/core": "2.0.0-beta.21",
|
|
19
|
+
"commander": "^15.0.0",
|
|
20
|
+
"find-package-json": "^1.2.0",
|
|
21
|
+
"semver": "^7.8.5"
|
|
29
22
|
},
|
|
30
23
|
"devDependencies": {
|
|
31
|
-
"@types/
|
|
32
|
-
"
|
|
24
|
+
"@types/find-package-json": "^1.2.7",
|
|
25
|
+
"@types/semver": "^7.8.0"
|
|
33
26
|
}
|
|
34
27
|
}
|
package/dist/bin/saws.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"saws.d.ts","sourceRoot":"","sources":["../../src/bin/saws.ts"],"names":[],"mappings":""}
|