@localctl/cli 0.2.0 → 0.4.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.
- package/LICENSE +21 -21
- package/README.md +398 -431
- package/assets/manifests/namespace-addons.yaml +4 -4
- package/assets/manifests/tls-store.yaml +8 -8
- package/bin/localctl.js +2 -2
- package/package.json +61 -54
- package/src/commands/addons.js +56 -56
- package/src/commands/doctor.js +44 -43
- package/src/commands/down.js +35 -35
- package/src/commands/exec.js +15 -15
- package/src/commands/generateManifests.js +12 -12
- package/src/commands/hosts.js +23 -23
- package/src/commands/logs.js +45 -45
- package/src/commands/profiles.js +180 -180
- package/src/commands/prune.js +49 -49
- package/src/commands/registryInfo.js +18 -18
- package/src/commands/reload.js +44 -44
- package/src/commands/secrets.js +75 -75
- package/src/commands/setup.js +36 -36
- package/src/commands/status.js +136 -136
- package/src/commands/up.js +123 -123
- package/src/index.js +38 -36
- package/src/lib/addonDirs.js +43 -43
- package/src/lib/addonLoader.js +190 -190
- package/src/lib/addons/cluster/logging/addon.yaml +46 -46
- package/src/lib/addons/cluster/logging/config/all-logs.json +21 -21
- package/src/lib/addons/cluster/monitoring/addon.yaml +33 -33
- package/src/lib/addons/mongo/addon.yaml +18 -18
- package/src/lib/addons/postgres/addon.yaml +39 -39
- package/src/lib/addons/redis/addon.yaml +29 -29
- package/src/lib/appSecrets.js +60 -60
- package/src/lib/clusterAddons.js +59 -59
- package/src/lib/clusterProvision.js +171 -170
- package/src/lib/configSchema.js +103 -103
- package/src/lib/constants.js +16 -16
- package/src/lib/engine.js +36 -20
- package/src/lib/exec.js +77 -77
- package/src/lib/hostsFile.js +79 -79
- package/src/lib/loadConfig.js +51 -51
- package/src/lib/logger.js +9 -9
- package/src/lib/manifestGen.js +210 -210
- package/src/lib/paths.js +41 -41
- package/src/lib/podman.js +72 -72
- package/src/lib/profileStore.js +134 -134
- package/src/lib/projectContext.js +32 -32
- package/src/lib/prompt.js +97 -97
- package/src/lib/toolInstall.js +30 -30
- package/src/lib/ui.js +50 -50
- package/src/lib/updateCheck.js +84 -0
- package/src/lib/updateCheckWorker.js +46 -0
- package/src/lib/workloadReadiness.js +35 -35
- package/templates/Tiltfile.tmpl +35 -35
package/src/lib/manifestGen.js
CHANGED
|
@@ -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 };
|