redkite 0.1.12 → 0.1.14
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 +62 -8
- package/dist/cli/agent.d.ts +5 -1
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/agent.js +7 -7
- package/dist/cli/agent.js.map +1 -1
- package/dist/cli/crash.js +4 -2
- package/dist/cli/crash.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +18 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +3 -2
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/screen.d.ts +3 -0
- package/dist/cli/screen.d.ts.map +1 -1
- package/dist/cli/screen.js +32 -6
- package/dist/cli/screen.js.map +1 -1
- package/dist/cli/viewer.d.ts.map +1 -1
- package/dist/cli/viewer.js +5 -5
- package/dist/cli/viewer.js.map +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +6 -3
- package/dist/deploy.js.map +1 -1
- package/dist/health.d.ts +2 -2
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +17 -9
- package/dist/health.js.map +1 -1
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -0
- package/dist/log.js.map +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/proxy.d.ts +5 -0
- package/dist/services/proxy.d.ts.map +1 -1
- package/dist/services/proxy.js +56 -2
- package/dist/services/proxy.js.map +1 -1
- package/dist/source.d.ts +2 -0
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +20 -0
- package/dist/source.js.map +1 -1
- package/dist/topology.d.ts.map +1 -1
- package/dist/topology.js +8 -1
- package/dist/topology.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/agent.ts +8 -8
- package/src/cli/crash.ts +4 -3
- package/src/cli/index.ts +21 -9
- package/src/cli/log.ts +3 -2
- package/src/cli/screen.ts +45 -4
- package/src/cli/viewer.ts +5 -4
- package/src/deploy.ts +7 -4
- package/src/health.ts +23 -16
- package/src/log.ts +19 -1
- package/src/services/index.ts +3 -0
- package/src/services/proxy.ts +71 -3
- package/src/source.ts +24 -0
- package/src/topology.ts +9 -1
- package/src/types.ts +18 -0
package/src/services/proxy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppTopology, Topology } from "../topology.js";
|
|
2
|
-
import type { Deployment, ProxySpec, ServiceSpec } from "../types.js";
|
|
2
|
+
import type { Deployment, ProxyLogs, ProxySpec, ServiceSpec } from "../types.js";
|
|
3
3
|
|
|
4
4
|
// The proxy every route is resolved by: what it is, and the configuration it
|
|
5
5
|
// runs. Both halves here, because a deployment does not list this service and
|
|
@@ -30,6 +30,9 @@ export const LISTEN_PORT = 3000;
|
|
|
30
30
|
// container claiming the first one's, so the config surface refuses it
|
|
31
31
|
export const PROXY = "nginx";
|
|
32
32
|
|
|
33
|
+
// Where nginx writes inside its container. A log directory is mounted here
|
|
34
|
+
export const LOG_DIRECTORY = "/var/log/nginx";
|
|
35
|
+
|
|
33
36
|
// Not something a deployment lists. Apps carry routes, routes need something
|
|
34
37
|
// to resolve them, and this says what that something is
|
|
35
38
|
export function proxyService(config: Deployment): ServiceSpec {
|
|
@@ -41,16 +44,22 @@ export function proxyService(config: Deployment): ServiceSpec {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export function renderProxy(topology: Topology, proxy: ProxySpec = {}) {
|
|
47
|
+
assertNotDerived(proxy.server ?? []);
|
|
48
|
+
assertLocations(topology, proxy);
|
|
49
|
+
|
|
44
50
|
const upstreams = topology.apps.map((app) => upstream(app)).join("\n\n");
|
|
45
51
|
// Longest route first, so "/api/" is not shadowed by the "/" catch-all
|
|
46
52
|
const ordered = [...topology.apps].sort((a, b) => b.route.length - a.route.length);
|
|
47
|
-
const locations = ordered.map((app) => location(app, proxy.location ?? [])).join("\n\n");
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
const locations = ordered
|
|
55
|
+
.map((app) => location(app, [...(proxy.location ?? []), ...(proxy.locations?.[app.name] ?? [])]))
|
|
56
|
+
.join("\n\n");
|
|
50
57
|
|
|
58
|
+
// Logging leads the lines written by hand, so an access_log there replaces it
|
|
51
59
|
const server = settled([
|
|
52
60
|
`merge_slashes off;`,
|
|
53
61
|
`client_max_body_size ${proxy.maxBodySize ?? "1M"};`,
|
|
62
|
+
...logging(proxy.logs, topology.environment),
|
|
54
63
|
...(proxy.server ?? []),
|
|
55
64
|
]);
|
|
56
65
|
|
|
@@ -101,6 +110,65 @@ function assertNotDerived(lines: string[]) {
|
|
|
101
110
|
);
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
// Nothing for a deployment that says nothing, so a proxy it already runs is not
|
|
114
|
+
// recreated for a config that means the same. error_log has no off: the
|
|
115
|
+
// quietest it gets is emerg, written nowhere
|
|
116
|
+
function logging(logs: false | ProxyLogs | undefined, environment: string) {
|
|
117
|
+
if (logs === undefined) return [];
|
|
118
|
+
if (logs === false) return ["access_log off;", "error_log /dev/null emerg;"];
|
|
119
|
+
|
|
120
|
+
// Named for the environment, since two environments on one host may be given
|
|
121
|
+
// the same directory
|
|
122
|
+
const access = logs.directory ? `${LOG_DIRECTORY}/${environment}.access.log` : "/dev/stdout";
|
|
123
|
+
const error = logs.directory ? `${LOG_DIRECTORY}/${environment}.error.log` : "/dev/stderr";
|
|
124
|
+
|
|
125
|
+
return [
|
|
126
|
+
logs.access === false ? "access_log off;" : `access_log ${access};`,
|
|
127
|
+
`error_log ${error} ${logs.errors ?? "error"};`,
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The host directory a deployment's logs land in, mounted where nginx writes.
|
|
132
|
+
// Absolute, because docker reads anything else as the name of a volume and the
|
|
133
|
+
// logs would go somewhere nobody asked for
|
|
134
|
+
export function logMount(proxy: ProxySpec | undefined) {
|
|
135
|
+
const directory = proxy?.logs ? proxy.logs.directory : undefined;
|
|
136
|
+
if (directory === undefined) return undefined;
|
|
137
|
+
|
|
138
|
+
if (!directory.startsWith("/") || /[:,]/.test(directory)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`The proxy logs to ${directory}, which has to be an absolute path on the deploy ` +
|
|
141
|
+
"host with no colon or comma in it: docker reads anything else as a volume name",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { volume: directory, mountPath: LOG_DIRECTORY };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// An app name nobody has is a typo, and a location it named would otherwise be
|
|
149
|
+
// settings nothing reads. proxy_pass is what routes the location to its app, so
|
|
150
|
+
// replacing it is not a setting but a broken route
|
|
151
|
+
function assertLocations(topology: Topology, proxy: ProxySpec) {
|
|
152
|
+
const apps = new Set(topology.apps.map((app) => app.name));
|
|
153
|
+
const unknown = Object.keys(proxy.locations ?? {}).filter((name) => !apps.has(name));
|
|
154
|
+
|
|
155
|
+
if (unknown.length > 0) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`The proxy sets locations for ${unknown.join(", ")}, which this deployment has ` +
|
|
158
|
+
`no app by that name for. Its apps are ${[...apps].join(", ")}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const lines = [...(proxy.location ?? []), ...Object.values(proxy.locations ?? {}).flat()];
|
|
163
|
+
const routing = lines.find((line) => directive(line) === "proxy_pass");
|
|
164
|
+
if (!routing) return;
|
|
165
|
+
|
|
166
|
+
throw new Error(
|
|
167
|
+
`A proxy location says ${routing.trim()}, and where a location sends a request ` +
|
|
168
|
+
"is its app's route, which redkite derives",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
104
172
|
function indented(lines: string[], by: number) {
|
|
105
173
|
return lines.map((line) => " ".repeat(by) + line).join("\n");
|
|
106
174
|
}
|
package/src/source.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Host } from "./host.js";
|
|
2
|
+
import { tag } from "./log.js";
|
|
2
3
|
|
|
3
4
|
// Getting the source onto the machine that builds it. A repository the host
|
|
4
5
|
// clones for itself over the forwarded agent, so no tree crosses the wire: a
|
|
@@ -45,6 +46,29 @@ const REVISION: Record<RefKind, (name: string) => string> = {
|
|
|
45
46
|
commit: (name) => `${name}^{commit}`,
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
// Hosts common enough to be worth a tag in place of their name
|
|
50
|
+
const HOSTS: Record<string, string> = { "github.com": "GH" };
|
|
51
|
+
|
|
52
|
+
// user@host:path, or scheme://user@host/path, with the .git every clone URL ends
|
|
53
|
+
// in. Anything that is not one of those, a directory for instance, has no host
|
|
54
|
+
const REMOTE = /^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@/]+@)?([^:/]+)[:/](.+?)(?:\.git)?\/?$/;
|
|
55
|
+
|
|
56
|
+
// How a repository is named on screen: the host as a tag where it is a known
|
|
57
|
+
// one, and the path without the .git that says nothing a person needs
|
|
58
|
+
export function describeRepo(repo: string) {
|
|
59
|
+
const match = REMOTE.exec(repo);
|
|
60
|
+
if (!match?.[1] || !match[2]) return repo.replace(/\.git$/, "");
|
|
61
|
+
|
|
62
|
+
const known = HOSTS[match[1]];
|
|
63
|
+
return known ? `${tag(known)} ${match[2]}` : `${match[1]}:${match[2]}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A branch is what is tracked nearly always, so it goes by its name alone. A tag
|
|
67
|
+
// or a commit is a pin, and says which kind it is
|
|
68
|
+
export function describeRef(ref: Ref) {
|
|
69
|
+
return ref.kind === "branch" ? ref.name : `${ref.kind} ${ref.name}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
48
72
|
// accept-new rather than no: a host key that changes is still worth refusing
|
|
49
73
|
const GIT_SSH = "ssh -o StrictHostKeyChecking=accept-new";
|
|
50
74
|
|
package/src/topology.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AppSpec, Deployment, ServiceSpec } from "./types.js";
|
|
|
2
2
|
|
|
3
3
|
import { environmentOf } from "./config.js";
|
|
4
4
|
import { mountFor } from "./layout.js";
|
|
5
|
+
import { logMount } from "./services/proxy.js";
|
|
5
6
|
|
|
6
7
|
// Every name and address the deployment uses, derived from the app list. This
|
|
7
8
|
// is the file that replaces the constants block: nothing is chosen by hand, so
|
|
@@ -92,7 +93,9 @@ export function topologyFor(config: Deployment, environment: string): Topology {
|
|
|
92
93
|
name: "nginx",
|
|
93
94
|
container: `${prefix}-nginx`,
|
|
94
95
|
address: address(NGINX_OCTET),
|
|
95
|
-
|
|
96
|
+
// Only a directory the deployment named. Nothing mounted otherwise, so a
|
|
97
|
+
// proxy that says nothing about its logs is the container it always was
|
|
98
|
+
volumes: routerVolumes(config),
|
|
96
99
|
},
|
|
97
100
|
apps,
|
|
98
101
|
services,
|
|
@@ -165,6 +168,11 @@ function serviceTopology(
|
|
|
165
168
|
};
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
function routerVolumes(config: Deployment) {
|
|
172
|
+
const logs = logMount(config.proxy);
|
|
173
|
+
return logs ? [logs] : [];
|
|
174
|
+
}
|
|
175
|
+
|
|
168
176
|
// Nginx resolves upstreams by container name, apps reach services by alias
|
|
169
177
|
function extraHosts(
|
|
170
178
|
apps: AppTopology[],
|
package/src/types.ts
CHANGED
|
@@ -92,6 +92,24 @@ export type ProxySpec = {
|
|
|
92
92
|
// Lines put inside every location, below what redkite sets, so one of these
|
|
93
93
|
// replaces a header rather than being replaced by it
|
|
94
94
|
location?: string[];
|
|
95
|
+
// Lines for one app's location alone, keyed by the app's name. Below the lines
|
|
96
|
+
// every location gets, so one of these replaces those for that app only
|
|
97
|
+
locations?: Record<string, string[]>;
|
|
98
|
+
// Where the proxy writes what it logs, and whether it logs at all. Absent
|
|
99
|
+
// leaves it to the image, which writes to the container's own output
|
|
100
|
+
logs?: false | ProxyLogs;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type ProxyLogs = {
|
|
104
|
+
// A directory on the deploy host, mounted where nginx writes its logs. Each
|
|
105
|
+
// environment writes files of its own into it. Absent keeps them on the
|
|
106
|
+
// container's output, where docker logs reads them
|
|
107
|
+
directory?: string;
|
|
108
|
+
// One line per request. On unless this says false
|
|
109
|
+
access?: boolean;
|
|
110
|
+
// How severe something has to be to reach the error log. nginx's own default
|
|
111
|
+
// is error
|
|
112
|
+
errors?: "debug" | "info" | "notice" | "warn" | "error" | "crit" | "alert" | "emerg";
|
|
95
113
|
};
|
|
96
114
|
|
|
97
115
|
export type ServiceSpec = {
|