@localctl/cli 0.2.0 → 0.3.0

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 (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +398 -431
  3. package/assets/manifests/namespace-addons.yaml +4 -4
  4. package/assets/manifests/tls-store.yaml +8 -8
  5. package/bin/localctl.js +2 -2
  6. package/package.json +61 -54
  7. package/src/commands/addons.js +56 -56
  8. package/src/commands/doctor.js +44 -43
  9. package/src/commands/down.js +35 -35
  10. package/src/commands/exec.js +15 -15
  11. package/src/commands/generateManifests.js +12 -12
  12. package/src/commands/hosts.js +23 -23
  13. package/src/commands/logs.js +45 -45
  14. package/src/commands/profiles.js +180 -180
  15. package/src/commands/prune.js +49 -49
  16. package/src/commands/registryInfo.js +18 -18
  17. package/src/commands/reload.js +44 -44
  18. package/src/commands/secrets.js +75 -75
  19. package/src/commands/setup.js +36 -36
  20. package/src/commands/status.js +136 -136
  21. package/src/commands/up.js +123 -123
  22. package/src/index.js +38 -36
  23. package/src/lib/addonDirs.js +43 -43
  24. package/src/lib/addonLoader.js +190 -190
  25. package/src/lib/addons/cluster/logging/addon.yaml +46 -46
  26. package/src/lib/addons/cluster/logging/config/all-logs.json +21 -21
  27. package/src/lib/addons/cluster/monitoring/addon.yaml +33 -33
  28. package/src/lib/addons/mongo/addon.yaml +18 -18
  29. package/src/lib/addons/postgres/addon.yaml +39 -39
  30. package/src/lib/addons/redis/addon.yaml +29 -29
  31. package/src/lib/appSecrets.js +60 -60
  32. package/src/lib/clusterAddons.js +59 -59
  33. package/src/lib/clusterProvision.js +171 -170
  34. package/src/lib/configSchema.js +103 -103
  35. package/src/lib/constants.js +16 -16
  36. package/src/lib/engine.js +36 -20
  37. package/src/lib/exec.js +77 -77
  38. package/src/lib/hostsFile.js +79 -79
  39. package/src/lib/loadConfig.js +51 -51
  40. package/src/lib/logger.js +9 -9
  41. package/src/lib/manifestGen.js +210 -210
  42. package/src/lib/paths.js +41 -41
  43. package/src/lib/podman.js +72 -72
  44. package/src/lib/profileStore.js +134 -134
  45. package/src/lib/projectContext.js +32 -32
  46. package/src/lib/prompt.js +97 -97
  47. package/src/lib/toolInstall.js +30 -30
  48. package/src/lib/ui.js +50 -50
  49. package/src/lib/updateCheck.js +84 -0
  50. package/src/lib/updateCheckWorker.js +46 -0
  51. package/src/lib/workloadReadiness.js +35 -35
  52. package/templates/Tiltfile.tmpl +35 -35
@@ -1,210 +1,210 @@
1
- const yaml = require('js-yaml');
2
- const { DOMAIN } = require('./constants');
3
- const { loadAddon } = require('./addonLoader');
4
- const { findAddonDir, listAddonNames } = require('./addonDirs');
5
- const { secretNameFor } = require('./appSecrets');
6
-
7
- function buildNamespace(cfg) {
8
- return { apiVersion: 'v1', kind: 'Namespace', metadata: { name: cfg.namespace } };
9
- }
10
-
11
- // A sidecar - an extra container sharing the pod's network namespace with the main app container.
12
- // No Service needed for the app to reach it: same pod means `localhost:<port>` already works.
13
- function buildSidecarContainer(sidecar) {
14
- const container = { name: sidecar.name, image: sidecar.image };
15
- if (sidecar.command) container.command = sidecar.command;
16
- if (sidecar.args) container.args = sidecar.args;
17
- if (sidecar.port) container.ports = [{ name: sidecar.name, containerPort: sidecar.port }];
18
- const envVars = Object.entries(sidecar.env || {}).map(([name, value]) => ({ name, value: String(value) }));
19
- if (envVars.length) container.env = envVars;
20
- return container;
21
- }
22
-
23
- // Defaults to a plain TCP check on the app's own port when nothing more specific is configured -
24
- // zero-config, but still real: `kubectl rollout status` succeeding used to only mean "the process
25
- // started," not "it's actually accepting connections." An explicit `path` upgrades it to an HTTP
26
- // check instead.
27
- function buildProbe(probeCfg, port, defaults) {
28
- const check = probeCfg && probeCfg.path
29
- ? { httpGet: { path: probeCfg.path, port } }
30
- : { tcpSocket: { port } };
31
- return {
32
- ...check,
33
- initialDelaySeconds: (probeCfg && probeCfg.initialDelaySeconds) ?? defaults.initialDelaySeconds,
34
- periodSeconds: (probeCfg && probeCfg.periodSeconds) ?? defaults.periodSeconds,
35
- };
36
- }
37
-
38
- // Blocks the main container from starting until a dependency's real Service is actually accepting
39
- // connections - without this, a dependency that isn't ready yet when the app's own container
40
- // starts is a race the app has to handle itself (or crash-loop until it wins it).
41
- function buildWaitInitContainer(depType, connect) {
42
- return {
43
- name: `wait-for-${depType}`,
44
- image: 'busybox:1.36',
45
- command: ['sh', '-c', `until nc -z -w1 ${connect.host} ${connect.port}; do echo "waiting for ${depType} (${connect.host}:${connect.port})..."; sleep 1; done`],
46
- };
47
- }
48
-
49
- function buildDeployment(cfg, envVars, initContainers) {
50
- const ports = [{ name: 'http', containerPort: cfg.port }];
51
- if (cfg.debug.enabled && cfg.debug.port) ports.push({ name: 'debug', containerPort: cfg.debug.port });
52
-
53
- return {
54
- apiVersion: 'apps/v1',
55
- kind: 'Deployment',
56
- metadata: {
57
- name: cfg.name,
58
- namespace: cfg.namespace,
59
- labels: { app: cfg.name, 'localctl.dev/managed': 'true' },
60
- },
61
- spec: {
62
- replicas: cfg.replicas,
63
- selector: { matchLabels: { app: cfg.name } },
64
- template: {
65
- metadata: { labels: { app: cfg.name } },
66
- spec: {
67
- initContainers: initContainers.length ? initContainers : undefined,
68
- containers: [
69
- {
70
- name: cfg.name,
71
- // Rewritten to the local registry by Tilt's default_registry()/docker_build().
72
- image: cfg.name,
73
- ports,
74
- env: envVars,
75
- readinessProbe: buildProbe(cfg.probes.readiness, cfg.port, { initialDelaySeconds: 3, periodSeconds: 10 }),
76
- livenessProbe: buildProbe(cfg.probes.liveness, cfg.port, { initialDelaySeconds: 10, periodSeconds: 20 }),
77
- resources: cfg.resources
78
- ? { requests: cfg.resources.requests, limits: cfg.resources.limits }
79
- : undefined,
80
- },
81
- ...cfg.sidecars.map(buildSidecarContainer),
82
- ],
83
- },
84
- },
85
- },
86
- };
87
- }
88
-
89
- function buildService(cfg) {
90
- const ports = [{ name: 'http', port: cfg.port, targetPort: cfg.port }];
91
- if (cfg.debug.enabled && cfg.debug.port) {
92
- ports.push({ name: 'debug', port: cfg.debug.port, targetPort: cfg.debug.port });
93
- }
94
- return {
95
- apiVersion: 'v1',
96
- kind: 'Service',
97
- metadata: {
98
- name: cfg.name,
99
- namespace: cfg.namespace,
100
- labels: { app: cfg.name, 'localctl.dev/managed': 'true' },
101
- },
102
- spec: { selector: { app: cfg.name }, ports },
103
- };
104
- }
105
-
106
- function buildIngress(cfg) {
107
- const host = `${cfg.subdomain}.${DOMAIN}`;
108
- const ingress = {
109
- apiVersion: 'networking.k8s.io/v1',
110
- kind: 'Ingress',
111
- metadata: {
112
- name: cfg.name,
113
- namespace: cfg.namespace,
114
- labels: { app: cfg.name, 'localctl.dev/managed': 'true' },
115
- annotations: {
116
- 'traefik.ingress.kubernetes.io/router.entrypoints': cfg.tls ? 'websecure' : 'web',
117
- },
118
- },
119
- spec: {
120
- rules: [
121
- {
122
- host,
123
- http: {
124
- paths: [
125
- {
126
- path: '/',
127
- pathType: 'Prefix',
128
- backend: { service: { name: cfg.name, port: { number: cfg.port } } },
129
- },
130
- ],
131
- },
132
- },
133
- ],
134
- },
135
- };
136
- // Empty secretName: Traefik falls back to the cluster's default TLSStore certificate
137
- // (the *.local.test cert installed by bin/bootstrap.sh / bootstrap.ps1).
138
- if (cfg.tls) ingress.spec.tls = [{ hosts: [host] }];
139
- return ingress;
140
- }
141
-
142
- // Every addon type is a folder named <type>/addon.yaml, found in your own addons dir
143
- // (~/.localctl/addons, or $LOCALCTL_ADDONS_DIR) if present, else the ones bundled with this CLI
144
- // (see docs/adding-tools.md) - there's no hardcoded list to keep in sync, dropping in a new folder
145
- // is enough to make a new dependency type usable, and a name in your own dir overrides a built-in
146
- // one of the same name.
147
- function knownAddonTypes() {
148
- return listAddonNames();
149
- }
150
-
151
- function generateManifests(cfg) {
152
- const literalEnv = { ...cfg.env };
153
- const secretEnv = {}; // envVarName -> { secret, key }
154
- const addonYamls = [];
155
- const initContainers = [];
156
-
157
- for (const dep of cfg.dependencies) {
158
- const addonDir = findAddonDir(dep.type);
159
- if (!addonDir) {
160
- throw new Error(
161
- `Unknown dependency type "${dep.type}" (no addon.yaml found for it in ~/.localctl/addons ` +
162
- `or the CLI's built-in addons). Known types: ${knownAddonTypes().join(', ')}. See ` +
163
- 'docs/adding-tools.md to add a new one.',
164
- );
165
- }
166
- const release = `${cfg.name}-${dep.type}`;
167
- const { manifestYaml, env, envFromSecret, connect } = loadAddon(addonDir, {
168
- release,
169
- namespace: cfg.namespace,
170
- addonName: dep.type,
171
- owner: cfg.name,
172
- chartVersionOverride: dep.version,
173
- extraVars: {
174
- database: dep.database || cfg.name,
175
- storage: dep.storage || '1Gi',
176
- },
177
- });
178
- addonYamls.push(manifestYaml);
179
- Object.assign(literalEnv, env);
180
- Object.assign(secretEnv, envFromSecret);
181
- if (connect) initContainers.push(buildWaitInitContainer(dep.type, connect));
182
- }
183
- // cfg.env (what the developer explicitly set) always wins over an addon's auto-injected default.
184
- Object.assign(literalEnv, cfg.env);
185
-
186
- // Your own secrets (API keys, third-party tokens - not addon-generated credentials): declared by
187
- // name only in config, values set separately via `localctl app secrets set`, injected the same
188
- // never-plaintext way as an addon's. configSchema.js already rejects a key appearing in both
189
- // "env" and "secrets", so no collision to resolve here.
190
- for (const key of cfg.secrets) {
191
- secretEnv[key] = { secret: secretNameFor(cfg), key };
192
- }
193
-
194
- const envVars = [
195
- ...Object.entries(literalEnv).map(([name, value]) => ({ name, value: String(value) })),
196
- ...Object.entries(secretEnv).map(([name, ref]) => ({
197
- name,
198
- valueFrom: { secretKeyRef: { name: ref.secret, key: ref.key } },
199
- })),
200
- ];
201
-
202
- const docs = [buildNamespace(cfg), buildDeployment(cfg, envVars, initContainers), buildService(cfg), buildIngress(cfg)];
203
- const rendered = docs.map((d) => yaml.dump(d)).join('---\n');
204
- // trimEnd() before joining: addon YAML doesn't reliably end with a trailing newline (see
205
- // addonLoader.js), and joining on a bare '---\n' in that case glues the separator directly onto
206
- // the previous piece's last line with no line break, corrupting the YAML.
207
- return [rendered, ...addonYamls].map((doc) => doc.trimEnd()).join('\n---\n');
208
- }
209
-
210
- module.exports = { generateManifests, knownAddonTypes };
1
+ const yaml = require('js-yaml');
2
+ const { DOMAIN } = require('./constants');
3
+ const { loadAddon } = require('./addonLoader');
4
+ const { findAddonDir, listAddonNames } = require('./addonDirs');
5
+ const { secretNameFor } = require('./appSecrets');
6
+
7
+ function buildNamespace(cfg) {
8
+ return { apiVersion: 'v1', kind: 'Namespace', metadata: { name: cfg.namespace } };
9
+ }
10
+
11
+ // A sidecar - an extra container sharing the pod's network namespace with the main app container.
12
+ // No Service needed for the app to reach it: same pod means `localhost:<port>` already works.
13
+ function buildSidecarContainer(sidecar) {
14
+ const container = { name: sidecar.name, image: sidecar.image };
15
+ if (sidecar.command) container.command = sidecar.command;
16
+ if (sidecar.args) container.args = sidecar.args;
17
+ if (sidecar.port) container.ports = [{ name: sidecar.name, containerPort: sidecar.port }];
18
+ const envVars = Object.entries(sidecar.env || {}).map(([name, value]) => ({ name, value: String(value) }));
19
+ if (envVars.length) container.env = envVars;
20
+ return container;
21
+ }
22
+
23
+ // Defaults to a plain TCP check on the app's own port when nothing more specific is configured -
24
+ // zero-config, but still real: `kubectl rollout status` succeeding used to only mean "the process
25
+ // started," not "it's actually accepting connections." An explicit `path` upgrades it to an HTTP
26
+ // check instead.
27
+ function buildProbe(probeCfg, port, defaults) {
28
+ const check = probeCfg && probeCfg.path
29
+ ? { httpGet: { path: probeCfg.path, port } }
30
+ : { tcpSocket: { port } };
31
+ return {
32
+ ...check,
33
+ initialDelaySeconds: (probeCfg && probeCfg.initialDelaySeconds) ?? defaults.initialDelaySeconds,
34
+ periodSeconds: (probeCfg && probeCfg.periodSeconds) ?? defaults.periodSeconds,
35
+ };
36
+ }
37
+
38
+ // Blocks the main container from starting until a dependency's real Service is actually accepting
39
+ // connections - without this, a dependency that isn't ready yet when the app's own container
40
+ // starts is a race the app has to handle itself (or crash-loop until it wins it).
41
+ function buildWaitInitContainer(depType, connect) {
42
+ return {
43
+ name: `wait-for-${depType}`,
44
+ image: 'busybox:1.36',
45
+ command: ['sh', '-c', `until nc -z -w1 ${connect.host} ${connect.port}; do echo "waiting for ${depType} (${connect.host}:${connect.port})..."; sleep 1; done`],
46
+ };
47
+ }
48
+
49
+ function buildDeployment(cfg, envVars, initContainers) {
50
+ const ports = [{ name: 'http', containerPort: cfg.port }];
51
+ if (cfg.debug.enabled && cfg.debug.port) ports.push({ name: 'debug', containerPort: cfg.debug.port });
52
+
53
+ return {
54
+ apiVersion: 'apps/v1',
55
+ kind: 'Deployment',
56
+ metadata: {
57
+ name: cfg.name,
58
+ namespace: cfg.namespace,
59
+ labels: { app: cfg.name, 'localctl.dev/managed': 'true' },
60
+ },
61
+ spec: {
62
+ replicas: cfg.replicas,
63
+ selector: { matchLabels: { app: cfg.name } },
64
+ template: {
65
+ metadata: { labels: { app: cfg.name } },
66
+ spec: {
67
+ initContainers: initContainers.length ? initContainers : undefined,
68
+ containers: [
69
+ {
70
+ name: cfg.name,
71
+ // Rewritten to the local registry by Tilt's default_registry()/docker_build().
72
+ image: cfg.name,
73
+ ports,
74
+ env: envVars,
75
+ readinessProbe: buildProbe(cfg.probes.readiness, cfg.port, { initialDelaySeconds: 3, periodSeconds: 10 }),
76
+ livenessProbe: buildProbe(cfg.probes.liveness, cfg.port, { initialDelaySeconds: 10, periodSeconds: 20 }),
77
+ resources: cfg.resources
78
+ ? { requests: cfg.resources.requests, limits: cfg.resources.limits }
79
+ : undefined,
80
+ },
81
+ ...cfg.sidecars.map(buildSidecarContainer),
82
+ ],
83
+ },
84
+ },
85
+ },
86
+ };
87
+ }
88
+
89
+ function buildService(cfg) {
90
+ const ports = [{ name: 'http', port: cfg.port, targetPort: cfg.port }];
91
+ if (cfg.debug.enabled && cfg.debug.port) {
92
+ ports.push({ name: 'debug', port: cfg.debug.port, targetPort: cfg.debug.port });
93
+ }
94
+ return {
95
+ apiVersion: 'v1',
96
+ kind: 'Service',
97
+ metadata: {
98
+ name: cfg.name,
99
+ namespace: cfg.namespace,
100
+ labels: { app: cfg.name, 'localctl.dev/managed': 'true' },
101
+ },
102
+ spec: { selector: { app: cfg.name }, ports },
103
+ };
104
+ }
105
+
106
+ function buildIngress(cfg) {
107
+ const host = `${cfg.subdomain}.${DOMAIN}`;
108
+ const ingress = {
109
+ apiVersion: 'networking.k8s.io/v1',
110
+ kind: 'Ingress',
111
+ metadata: {
112
+ name: cfg.name,
113
+ namespace: cfg.namespace,
114
+ labels: { app: cfg.name, 'localctl.dev/managed': 'true' },
115
+ annotations: {
116
+ 'traefik.ingress.kubernetes.io/router.entrypoints': cfg.tls ? 'websecure' : 'web',
117
+ },
118
+ },
119
+ spec: {
120
+ rules: [
121
+ {
122
+ host,
123
+ http: {
124
+ paths: [
125
+ {
126
+ path: '/',
127
+ pathType: 'Prefix',
128
+ backend: { service: { name: cfg.name, port: { number: cfg.port } } },
129
+ },
130
+ ],
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ };
136
+ // Empty secretName: Traefik falls back to the cluster's default TLSStore certificate
137
+ // (the *.local.test cert installed by bin/bootstrap.sh / bootstrap.ps1).
138
+ if (cfg.tls) ingress.spec.tls = [{ hosts: [host] }];
139
+ return ingress;
140
+ }
141
+
142
+ // Every addon type is a folder named <type>/addon.yaml, found in your own addons dir
143
+ // (~/.localctl/addons, or $LOCALCTL_ADDONS_DIR) if present, else the ones bundled with this CLI
144
+ // (see docs/adding-tools.md) - there's no hardcoded list to keep in sync, dropping in a new folder
145
+ // is enough to make a new dependency type usable, and a name in your own dir overrides a built-in
146
+ // one of the same name.
147
+ function knownAddonTypes() {
148
+ return listAddonNames();
149
+ }
150
+
151
+ function generateManifests(cfg) {
152
+ const literalEnv = { ...cfg.env };
153
+ const secretEnv = {}; // envVarName -> { secret, key }
154
+ const addonYamls = [];
155
+ const initContainers = [];
156
+
157
+ for (const dep of cfg.dependencies) {
158
+ const addonDir = findAddonDir(dep.type);
159
+ if (!addonDir) {
160
+ throw new Error(
161
+ `Unknown dependency type "${dep.type}" (no addon.yaml found for it in ~/.localctl/addons ` +
162
+ `or the CLI's built-in addons). Known types: ${knownAddonTypes().join(', ')}. See ` +
163
+ 'docs/adding-tools.md to add a new one.',
164
+ );
165
+ }
166
+ const release = `${cfg.name}-${dep.type}`;
167
+ const { manifestYaml, env, envFromSecret, connect } = loadAddon(addonDir, {
168
+ release,
169
+ namespace: cfg.namespace,
170
+ addonName: dep.type,
171
+ owner: cfg.name,
172
+ chartVersionOverride: dep.version,
173
+ extraVars: {
174
+ database: dep.database || cfg.name,
175
+ storage: dep.storage || '1Gi',
176
+ },
177
+ });
178
+ addonYamls.push(manifestYaml);
179
+ Object.assign(literalEnv, env);
180
+ Object.assign(secretEnv, envFromSecret);
181
+ if (connect) initContainers.push(buildWaitInitContainer(dep.type, connect));
182
+ }
183
+ // cfg.env (what the developer explicitly set) always wins over an addon's auto-injected default.
184
+ Object.assign(literalEnv, cfg.env);
185
+
186
+ // Your own secrets (API keys, third-party tokens - not addon-generated credentials): declared by
187
+ // name only in config, values set separately via `localctl app secrets set`, injected the same
188
+ // never-plaintext way as an addon's. configSchema.js already rejects a key appearing in both
189
+ // "env" and "secrets", so no collision to resolve here.
190
+ for (const key of cfg.secrets) {
191
+ secretEnv[key] = { secret: secretNameFor(cfg), key };
192
+ }
193
+
194
+ const envVars = [
195
+ ...Object.entries(literalEnv).map(([name, value]) => ({ name, value: String(value) })),
196
+ ...Object.entries(secretEnv).map(([name, ref]) => ({
197
+ name,
198
+ valueFrom: { secretKeyRef: { name: ref.secret, key: ref.key } },
199
+ })),
200
+ ];
201
+
202
+ const docs = [buildNamespace(cfg), buildDeployment(cfg, envVars, initContainers), buildService(cfg), buildIngress(cfg)];
203
+ const rendered = docs.map((d) => yaml.dump(d)).join('---\n');
204
+ // trimEnd() before joining: addon YAML doesn't reliably end with a trailing newline (see
205
+ // addonLoader.js), and joining on a bare '---\n' in that case glues the separator directly onto
206
+ // the previous piece's last line with no line break, corrupting the YAML.
207
+ return [rendered, ...addonYamls].map((doc) => doc.trimEnd()).join('\n---\n');
208
+ }
209
+
210
+ module.exports = { generateManifests, knownAddonTypes };
package/src/lib/paths.js CHANGED
@@ -1,41 +1,41 @@
1
- const path = require('path');
2
- const os = require('os');
3
- const fs = require('fs');
4
-
5
- const APP_DIR = process.cwd();
6
- const LOCAL_DIR = path.join(APP_DIR, '.local');
7
- const CONFIG_PATH = path.join(LOCAL_DIR, 'config.json');
8
- const HOME_STATE_DIR = path.join(os.homedir(), '.localctl');
9
- const APPS_REGISTRY_PATH = path.join(HOME_STATE_DIR, 'apps.json');
10
- const PROFILES_PATH = path.join(HOME_STATE_DIR, 'profiles.json');
11
- const RUN_DIR = path.join(HOME_STATE_DIR, 'run');
12
-
13
- function ensureHomeState() {
14
- if (!fs.existsSync(HOME_STATE_DIR)) fs.mkdirSync(HOME_STATE_DIR, { recursive: true });
15
- if (!fs.existsSync(APPS_REGISTRY_PATH)) fs.writeFileSync(APPS_REGISTRY_PATH, '{}');
16
- }
17
-
18
- // One {pidFile, portFile, logFile} set per app name, used by `localctl up` (detached mode),
19
- // `localctl down`, and `localctl reload` to track/clean up/talk to a background Tilt process.
20
- // portFile holds the random port that app's Tilt instance's web/API server is bound to (each app
21
- // gets its own - see up.js/getFreePort), needed to address it with `tilt trigger --port <n>`.
22
- function runFiles(name) {
23
- if (!fs.existsSync(RUN_DIR)) fs.mkdirSync(RUN_DIR, { recursive: true });
24
- return {
25
- pidFile: path.join(RUN_DIR, `${name}.pid`),
26
- portFile: path.join(RUN_DIR, `${name}.port`),
27
- logFile: path.join(RUN_DIR, `${name}.log`),
28
- };
29
- }
30
-
31
- module.exports = {
32
- APP_DIR,
33
- LOCAL_DIR,
34
- CONFIG_PATH,
35
- HOME_STATE_DIR,
36
- APPS_REGISTRY_PATH,
37
- PROFILES_PATH,
38
- RUN_DIR,
39
- ensureHomeState,
40
- runFiles,
41
- };
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs');
4
+
5
+ const APP_DIR = process.cwd();
6
+ const LOCAL_DIR = path.join(APP_DIR, '.local');
7
+ const CONFIG_PATH = path.join(LOCAL_DIR, 'config.json');
8
+ const HOME_STATE_DIR = path.join(os.homedir(), '.localctl');
9
+ const APPS_REGISTRY_PATH = path.join(HOME_STATE_DIR, 'apps.json');
10
+ const PROFILES_PATH = path.join(HOME_STATE_DIR, 'profiles.json');
11
+ const RUN_DIR = path.join(HOME_STATE_DIR, 'run');
12
+
13
+ function ensureHomeState() {
14
+ if (!fs.existsSync(HOME_STATE_DIR)) fs.mkdirSync(HOME_STATE_DIR, { recursive: true });
15
+ if (!fs.existsSync(APPS_REGISTRY_PATH)) fs.writeFileSync(APPS_REGISTRY_PATH, '{}');
16
+ }
17
+
18
+ // One {pidFile, portFile, logFile} set per app name, used by `localctl up` (detached mode),
19
+ // `localctl down`, and `localctl reload` to track/clean up/talk to a background Tilt process.
20
+ // portFile holds the random port that app's Tilt instance's web/API server is bound to (each app
21
+ // gets its own - see up.js/getFreePort), needed to address it with `tilt trigger --port <n>`.
22
+ function runFiles(name) {
23
+ if (!fs.existsSync(RUN_DIR)) fs.mkdirSync(RUN_DIR, { recursive: true });
24
+ return {
25
+ pidFile: path.join(RUN_DIR, `${name}.pid`),
26
+ portFile: path.join(RUN_DIR, `${name}.port`),
27
+ logFile: path.join(RUN_DIR, `${name}.log`),
28
+ };
29
+ }
30
+
31
+ module.exports = {
32
+ APP_DIR,
33
+ LOCAL_DIR,
34
+ CONFIG_PATH,
35
+ HOME_STATE_DIR,
36
+ APPS_REGISTRY_PATH,
37
+ PROFILES_PATH,
38
+ RUN_DIR,
39
+ ensureHomeState,
40
+ runFiles,
41
+ };
package/src/lib/podman.js CHANGED
@@ -1,72 +1,72 @@
1
- const { run, capture } = require('./exec');
2
- const logger = require('./logger');
3
-
4
- const CONF_PATH = '/etc/containers/registries.conf.d/999-local-dev.conf';
5
-
6
- function machineName() {
7
- const res = capture('podman', ['machine', 'list', '--format', '{{.Name}}']);
8
- const [first] = res.stdout.split('\n').filter(Boolean);
9
- return first || null;
10
- }
11
-
12
- function registryBlock(hostPort) {
13
- return [
14
- '[[registry]]',
15
- `location = "localhost:${hostPort}"`,
16
- 'insecure = true',
17
- '',
18
- '[[registry]]',
19
- `location = "127.0.0.1:${hostPort}"`,
20
- 'insecure = true',
21
- '',
22
- ].join('\n');
23
- }
24
-
25
- // Podman's daemon runs inside a lightweight VM on macOS/Windows - registries.conf lives inside
26
- // that VM, not on the host, so a host-side config file can't mark our (deliberately TLS-less)
27
- // local registry as insecure. Done before the cluster/registry even exist, because applying it
28
- // needs a VM restart, which would otherwise kill and require restarting every container already
29
- // running.
30
- //
31
- // Appends this port's block to whatever's already there instead of overwriting the file: a
32
- // second Main project (its own registry port) provisioned after the first would otherwise replace
33
- // the first project's trust entry outright, silently breaking its push access the next time
34
- // something tried to use it. One file, one block per port ever provisioned on this machine.
35
- function ensureInsecureRegistry(hostPort) {
36
- const machine = machineName();
37
- if (!machine) return;
38
-
39
- const current = capture('podman', ['machine', 'ssh', machine, `cat ${CONF_PATH} 2>/dev/null`]).stdout;
40
- if (current.includes(`location = "localhost:${hostPort}"`)) {
41
- logger.success(`Podman VM already trusts localhost:${hostPort} as an insecure registry`);
42
- return;
43
- }
44
-
45
- const next = `${current.trimEnd()}\n\n${registryBlock(hostPort)}`.trim().concat('\n');
46
- logger.step(`Marking localhost:${hostPort} as an insecure registry inside the Podman VM...`);
47
- run('podman', ['machine', 'ssh', machine, 'sudo mkdir -p /etc/containers/registries.conf.d']);
48
- run('podman', ['machine', 'ssh', machine, `sudo tee ${CONF_PATH} > /dev/null`], {
49
- stdio: ['pipe', 'inherit', 'inherit'],
50
- input: next,
51
- });
52
- logger.step('Restarting Podman machine to apply...');
53
- run('podman', ['machine', 'stop', machine], { stdio: 'ignore' });
54
- run('podman', ['machine', 'start', machine], { stdio: 'ignore' });
55
- logger.success('Podman machine restarted');
56
- }
57
-
58
- function teardownInsecureRegistry() {
59
- const machine = machineName();
60
- if (!machine) return;
61
-
62
- const exists = capture('podman', ['machine', 'ssh', machine, `test -f ${CONF_PATH} && echo yes`]).stdout;
63
- if (exists.trim() !== 'yes') return;
64
-
65
- logger.step('Removing insecure-registry config from the Podman VM...');
66
- run('podman', ['machine', 'ssh', machine, `sudo rm -f ${CONF_PATH}`], { stdio: 'ignore', allowFail: true });
67
- run('podman', ['machine', 'stop', machine], { stdio: 'ignore', allowFail: true });
68
- run('podman', ['machine', 'start', machine], { stdio: 'ignore', allowFail: true });
69
- logger.success('Podman VM config removed and machine restarted');
70
- }
71
-
72
- module.exports = { ensureInsecureRegistry, teardownInsecureRegistry };
1
+ const { run, capture } = require('./exec');
2
+ const logger = require('./logger');
3
+
4
+ const CONF_PATH = '/etc/containers/registries.conf.d/999-local-dev.conf';
5
+
6
+ function machineName() {
7
+ const res = capture('podman', ['machine', 'list', '--format', '{{.Name}}']);
8
+ const [first] = res.stdout.split('\n').filter(Boolean);
9
+ return first || null;
10
+ }
11
+
12
+ function registryBlock(hostPort) {
13
+ return [
14
+ '[[registry]]',
15
+ `location = "localhost:${hostPort}"`,
16
+ 'insecure = true',
17
+ '',
18
+ '[[registry]]',
19
+ `location = "127.0.0.1:${hostPort}"`,
20
+ 'insecure = true',
21
+ '',
22
+ ].join('\n');
23
+ }
24
+
25
+ // Podman's daemon runs inside a lightweight VM on macOS/Windows - registries.conf lives inside
26
+ // that VM, not on the host, so a host-side config file can't mark our (deliberately TLS-less)
27
+ // local registry as insecure. Done before the cluster/registry even exist, because applying it
28
+ // needs a VM restart, which would otherwise kill and require restarting every container already
29
+ // running.
30
+ //
31
+ // Appends this port's block to whatever's already there instead of overwriting the file: a
32
+ // second Main project (its own registry port) provisioned after the first would otherwise replace
33
+ // the first project's trust entry outright, silently breaking its push access the next time
34
+ // something tried to use it. One file, one block per port ever provisioned on this machine.
35
+ function ensureInsecureRegistry(hostPort) {
36
+ const machine = machineName();
37
+ if (!machine) return;
38
+
39
+ const current = capture('podman', ['machine', 'ssh', machine, `cat ${CONF_PATH} 2>/dev/null`]).stdout;
40
+ if (current.includes(`location = "localhost:${hostPort}"`)) {
41
+ logger.success(`Podman VM already trusts localhost:${hostPort} as an insecure registry`);
42
+ return;
43
+ }
44
+
45
+ const next = `${current.trimEnd()}\n\n${registryBlock(hostPort)}`.trim().concat('\n');
46
+ logger.step(`Marking localhost:${hostPort} as an insecure registry inside the Podman VM...`);
47
+ run('podman', ['machine', 'ssh', machine, 'sudo mkdir -p /etc/containers/registries.conf.d']);
48
+ run('podman', ['machine', 'ssh', machine, `sudo tee ${CONF_PATH} > /dev/null`], {
49
+ stdio: ['pipe', 'inherit', 'inherit'],
50
+ input: next,
51
+ });
52
+ logger.step('Restarting Podman machine to apply...');
53
+ run('podman', ['machine', 'stop', machine], { stdio: 'ignore' });
54
+ run('podman', ['machine', 'start', machine], { stdio: 'ignore' });
55
+ logger.success('Podman machine restarted');
56
+ }
57
+
58
+ function teardownInsecureRegistry() {
59
+ const machine = machineName();
60
+ if (!machine) return;
61
+
62
+ const exists = capture('podman', ['machine', 'ssh', machine, `test -f ${CONF_PATH} && echo yes`]).stdout;
63
+ if (exists.trim() !== 'yes') return;
64
+
65
+ logger.step('Removing insecure-registry config from the Podman VM...');
66
+ run('podman', ['machine', 'ssh', machine, `sudo rm -f ${CONF_PATH}`], { stdio: 'ignore', allowFail: true });
67
+ run('podman', ['machine', 'stop', machine], { stdio: 'ignore', allowFail: true });
68
+ run('podman', ['machine', 'start', machine], { stdio: 'ignore', allowFail: true });
69
+ logger.success('Podman VM config removed and machine restarted');
70
+ }
71
+
72
+ module.exports = { ensureInsecureRegistry, teardownInsecureRegistry };