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.
Files changed (62) hide show
  1. package/README.md +62 -8
  2. package/dist/cli/agent.d.ts +5 -1
  3. package/dist/cli/agent.d.ts.map +1 -1
  4. package/dist/cli/agent.js +7 -7
  5. package/dist/cli/agent.js.map +1 -1
  6. package/dist/cli/crash.js +4 -2
  7. package/dist/cli/crash.js.map +1 -1
  8. package/dist/cli/index.d.ts.map +1 -1
  9. package/dist/cli/index.js +18 -10
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/cli/log.d.ts.map +1 -1
  12. package/dist/cli/log.js +3 -2
  13. package/dist/cli/log.js.map +1 -1
  14. package/dist/cli/screen.d.ts +3 -0
  15. package/dist/cli/screen.d.ts.map +1 -1
  16. package/dist/cli/screen.js +32 -6
  17. package/dist/cli/screen.js.map +1 -1
  18. package/dist/cli/viewer.d.ts.map +1 -1
  19. package/dist/cli/viewer.js +5 -5
  20. package/dist/cli/viewer.js.map +1 -1
  21. package/dist/deploy.d.ts.map +1 -1
  22. package/dist/deploy.js +6 -3
  23. package/dist/deploy.js.map +1 -1
  24. package/dist/health.d.ts +2 -2
  25. package/dist/health.d.ts.map +1 -1
  26. package/dist/health.js +17 -9
  27. package/dist/health.js.map +1 -1
  28. package/dist/log.d.ts +3 -0
  29. package/dist/log.d.ts.map +1 -1
  30. package/dist/log.js +13 -0
  31. package/dist/log.js.map +1 -1
  32. package/dist/services/index.d.ts.map +1 -1
  33. package/dist/services/index.js +3 -0
  34. package/dist/services/index.js.map +1 -1
  35. package/dist/services/proxy.d.ts +5 -0
  36. package/dist/services/proxy.d.ts.map +1 -1
  37. package/dist/services/proxy.js +56 -2
  38. package/dist/services/proxy.js.map +1 -1
  39. package/dist/source.d.ts +2 -0
  40. package/dist/source.d.ts.map +1 -1
  41. package/dist/source.js +20 -0
  42. package/dist/source.js.map +1 -1
  43. package/dist/topology.d.ts.map +1 -1
  44. package/dist/topology.js +8 -1
  45. package/dist/topology.js.map +1 -1
  46. package/dist/types.d.ts +7 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/cli/agent.ts +8 -8
  50. package/src/cli/crash.ts +4 -3
  51. package/src/cli/index.ts +21 -9
  52. package/src/cli/log.ts +3 -2
  53. package/src/cli/screen.ts +45 -4
  54. package/src/cli/viewer.ts +5 -4
  55. package/src/deploy.ts +7 -4
  56. package/src/health.ts +23 -16
  57. package/src/log.ts +19 -1
  58. package/src/services/index.ts +3 -0
  59. package/src/services/proxy.ts +71 -3
  60. package/src/source.ts +24 -0
  61. package/src/topology.ts +9 -1
  62. package/src/types.ts +18 -0
@@ -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
- assertNotDerived(proxy.server ?? []);
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
- volumes: [],
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 = {