@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
|
@@ -1,170 +1,171 @@
|
|
|
1
|
-
const os = require('os');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const yaml = require('js-yaml');
|
|
5
|
-
const { run, capture, sleepSync, commandExists } = require('./exec');
|
|
6
|
-
const { detectEngine } = require('./engine');
|
|
7
|
-
const { installIfMissing } = require('./toolInstall');
|
|
8
|
-
const { ensureInsecureRegistry } = require('./podman');
|
|
9
|
-
const { DOMAIN } = require('./constants');
|
|
10
|
-
const logger = require('./logger');
|
|
11
|
-
|
|
12
|
-
// Cluster-level manifests: bundled under cli/assets/ in the npm package (copied there by
|
|
13
|
-
// scripts/sync-assets.js at pack time), or read straight from the repo root in a git checkout.
|
|
14
|
-
const BUNDLED_MANIFESTS_DIR = path.resolve(__dirname, '..', '..', 'assets', 'manifests');
|
|
15
|
-
const MANIFESTS_DIR = fs.existsSync(BUNDLED_MANIFESTS_DIR)
|
|
16
|
-
? BUNDLED_MANIFESTS_DIR
|
|
17
|
-
: path.resolve(__dirname, '..', '..', '..', 'cluster', 'manifests');
|
|
18
|
-
|
|
19
|
-
function registryNameFor(clusterName) {
|
|
20
|
-
return `${clusterName}-registry`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function clusterExists(clusterName) {
|
|
24
|
-
return capture('k3d', ['cluster', 'list']).stdout
|
|
25
|
-
.split('\n')
|
|
26
|
-
.map((line) => line.trim().split(/\s+/)[0])
|
|
27
|
-
.includes(clusterName);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Brings up (idempotently) one Main project's cluster: container engine + tool checks, the k3d
|
|
31
|
-
// cluster itself (own ports, since two clusters can't both bind host :80/:443), its own local
|
|
32
|
-
// registry container, and the shared *.local.test TLS cert wired into its Traefik. Used by both
|
|
33
|
-
// `localctl setup` (the "default" profile, unchanged ports/names from before multi-project
|
|
34
|
-
// support) and `localctl profiles new`/`switch` (any other Main project).
|
|
35
|
-
function provisionCluster({ clusterName, httpPort, httpsPort, registryPort }) {
|
|
36
|
-
logger.step('Detecting container engine...');
|
|
37
|
-
const detected = detectEngine();
|
|
38
|
-
if (!detected) {
|
|
39
|
-
throw new Error('No running container engine found. Start Docker Desktop, Podman, or Rancher Desktop and re-run.');
|
|
40
|
-
}
|
|
41
|
-
const { engine, bin: engineBin } = detected;
|
|
42
|
-
logger.success(`Using container engine: ${engine} (${engineBin} CLI)`);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
installIfMissing('
|
|
53
|
-
installIfMissing('
|
|
54
|
-
installIfMissing('
|
|
55
|
-
installIfMissing('
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{ port: `${
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
'
|
|
100
|
-
'--
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
run('kubectl', ['apply', '-f', path.join(MANIFESTS_DIR, '
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
run(engineBin, ['
|
|
155
|
-
run(engineBin, ['
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
5
|
+
const { run, capture, sleepSync, commandExists } = require('./exec');
|
|
6
|
+
const { detectEngine, warnIfExperimental } = require('./engine');
|
|
7
|
+
const { installIfMissing } = require('./toolInstall');
|
|
8
|
+
const { ensureInsecureRegistry } = require('./podman');
|
|
9
|
+
const { DOMAIN } = require('./constants');
|
|
10
|
+
const logger = require('./logger');
|
|
11
|
+
|
|
12
|
+
// Cluster-level manifests: bundled under cli/assets/ in the npm package (copied there by
|
|
13
|
+
// scripts/sync-assets.js at pack time), or read straight from the repo root in a git checkout.
|
|
14
|
+
const BUNDLED_MANIFESTS_DIR = path.resolve(__dirname, '..', '..', 'assets', 'manifests');
|
|
15
|
+
const MANIFESTS_DIR = fs.existsSync(BUNDLED_MANIFESTS_DIR)
|
|
16
|
+
? BUNDLED_MANIFESTS_DIR
|
|
17
|
+
: path.resolve(__dirname, '..', '..', '..', 'cluster', 'manifests');
|
|
18
|
+
|
|
19
|
+
function registryNameFor(clusterName) {
|
|
20
|
+
return `${clusterName}-registry`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clusterExists(clusterName) {
|
|
24
|
+
return capture('k3d', ['cluster', 'list']).stdout
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim().split(/\s+/)[0])
|
|
27
|
+
.includes(clusterName);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Brings up (idempotently) one Main project's cluster: container engine + tool checks, the k3d
|
|
31
|
+
// cluster itself (own ports, since two clusters can't both bind host :80/:443), its own local
|
|
32
|
+
// registry container, and the shared *.local.test TLS cert wired into its Traefik. Used by both
|
|
33
|
+
// `localctl setup` (the "default" profile, unchanged ports/names from before multi-project
|
|
34
|
+
// support) and `localctl profiles new`/`switch` (any other Main project).
|
|
35
|
+
function provisionCluster({ clusterName, httpPort, httpsPort, registryPort }) {
|
|
36
|
+
logger.step('Detecting container engine...');
|
|
37
|
+
const detected = detectEngine();
|
|
38
|
+
if (!detected) {
|
|
39
|
+
throw new Error('No running container engine found. Start Docker Desktop, Podman, or Rancher Desktop and re-run.');
|
|
40
|
+
}
|
|
41
|
+
const { engine, bin: engineBin } = detected;
|
|
42
|
+
logger.success(`Using container engine: ${engine} (${engineBin} CLI)`);
|
|
43
|
+
warnIfExperimental(engine);
|
|
44
|
+
|
|
45
|
+
// Podman-specific, and must happen before the cluster/registry exist - see lib/podman.js. The
|
|
46
|
+
// insecure-registry trust is VM-wide, so it only needs doing once no matter how many clusters
|
|
47
|
+
// exist, but re-running it per registry port is cheap and idempotent.
|
|
48
|
+
if (engine === 'podman') {
|
|
49
|
+
ensureInsecureRegistry(registryPort);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
installIfMissing('k3d', 'k3d', 'k3d-io.k3d');
|
|
53
|
+
installIfMissing('kubectl', 'kubernetes-cli', 'Kubernetes.kubectl');
|
|
54
|
+
installIfMissing('tilt', 'tilt', 'Tilt.dev.tilt');
|
|
55
|
+
installIfMissing('mkcert', 'mkcert', 'FiloSottile.mkcert');
|
|
56
|
+
installIfMissing('helm', 'helm', 'Helm.Helm');
|
|
57
|
+
|
|
58
|
+
const registryName = registryNameFor(clusterName);
|
|
59
|
+
|
|
60
|
+
if (clusterExists(clusterName)) {
|
|
61
|
+
logger.success(`k3d cluster '${clusterName}' already exists`);
|
|
62
|
+
} else {
|
|
63
|
+
logger.step(`Creating k3d cluster '${clusterName}'...`);
|
|
64
|
+
// Generated fresh per call, not a static file: every Main project needs its own cluster
|
|
65
|
+
// name, ports, and registry mirror name baked in.
|
|
66
|
+
const k3dConfig = {
|
|
67
|
+
apiVersion: 'k3d.io/v1alpha5',
|
|
68
|
+
kind: 'Simple',
|
|
69
|
+
metadata: { name: clusterName },
|
|
70
|
+
servers: 1,
|
|
71
|
+
agents: 0,
|
|
72
|
+
ports: [
|
|
73
|
+
{ port: `${httpPort}:80`, nodeFilters: ['loadbalancer'] },
|
|
74
|
+
{ port: `${httpsPort}:443`, nodeFilters: ['loadbalancer'] },
|
|
75
|
+
],
|
|
76
|
+
// Plain container + mirror config, not k3d's built-in `registries.create`: that hard-codes
|
|
77
|
+
// attaching to a network literally named "bridge", which Docker has by default but Podman
|
|
78
|
+
// does not, breaking cluster creation there.
|
|
79
|
+
registries: {
|
|
80
|
+
config: yaml.dump({ mirrors: { [`${registryName}:5000`]: { endpoint: [`http://${registryName}:5000`] } } }),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const tmpConfig = path.join(os.tmpdir(), `localctl-k3d-config-${clusterName}.yaml`);
|
|
84
|
+
fs.writeFileSync(tmpConfig, yaml.dump(k3dConfig));
|
|
85
|
+
run('k3d', ['cluster', 'create', '--config', tmpConfig, '--wait']);
|
|
86
|
+
fs.rmSync(tmpConfig, { force: true });
|
|
87
|
+
logger.success('Cluster created');
|
|
88
|
+
}
|
|
89
|
+
run('kubectl', ['config', 'use-context', `k3d-${clusterName}`], { stdio: 'ignore' });
|
|
90
|
+
|
|
91
|
+
const registryNetwork = `k3d-${clusterName}`;
|
|
92
|
+
const existingNames = capture(engineBin, ['ps', '-a', '--format', '{{.Names}}']).stdout.split('\n');
|
|
93
|
+
if (existingNames.includes(registryName)) {
|
|
94
|
+
logger.success('Registry container already exists');
|
|
95
|
+
run(engineBin, ['start', registryName], { stdio: 'ignore', allowFail: true });
|
|
96
|
+
} else {
|
|
97
|
+
logger.step('Starting local image registry...');
|
|
98
|
+
run(engineBin, [
|
|
99
|
+
'run', '-d',
|
|
100
|
+
'--name', registryName,
|
|
101
|
+
'--network', registryNetwork,
|
|
102
|
+
'-p', `0.0.0.0:${registryPort}:5000`,
|
|
103
|
+
'--restart=always',
|
|
104
|
+
'registry:2',
|
|
105
|
+
]);
|
|
106
|
+
logger.success(`Registry running at localhost:${registryPort} (in-cluster name: ${registryName}:5000)`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// TLS via mkcert - one *.local.test cert/key, shared across every cluster (it's the same
|
|
110
|
+
// domain everywhere; only the namespace/subdomain prefix or the cluster differs).
|
|
111
|
+
logger.step(`Setting up local TLS for *.${DOMAIN}...`);
|
|
112
|
+
run('mkcert', ['-install']);
|
|
113
|
+
const certDir = path.join(os.homedir(), '.localctl', 'certs');
|
|
114
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
115
|
+
const certFile = path.join(certDir, 'local-dev.pem');
|
|
116
|
+
const keyFile = path.join(certDir, 'local-dev-key.pem');
|
|
117
|
+
if (!fs.existsSync(certFile) || !fs.existsSync(keyFile)) {
|
|
118
|
+
run('mkcert', ['-cert-file', certFile, '-key-file', keyFile, `*.${DOMAIN}`, DOMAIN]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const secretYaml = capture('kubectl', [
|
|
122
|
+
'create', 'secret', 'tls', 'local-dev-tls',
|
|
123
|
+
`--cert=${certFile}`, `--key=${keyFile}`,
|
|
124
|
+
'-n', 'kube-system', '--dry-run=client', '-o', 'yaml',
|
|
125
|
+
]).stdout;
|
|
126
|
+
run('kubectl', ['apply', '-f', '-'], { stdio: ['pipe', 'inherit', 'inherit'], input: secretYaml });
|
|
127
|
+
|
|
128
|
+
// k3d's --wait only waits for node readiness, not for Traefik's helm-install job to finish
|
|
129
|
+
// registering its CRDs, so applying TLSStore any earlier races and fails intermittently.
|
|
130
|
+
logger.step('Waiting for Traefik CRDs...');
|
|
131
|
+
for (let i = 0; i < 60; i += 1) {
|
|
132
|
+
if (capture('kubectl', ['get', 'crd', 'tlsstores.traefik.io']).status === 0) break;
|
|
133
|
+
sleepSync(2000);
|
|
134
|
+
}
|
|
135
|
+
run('kubectl', ['wait', '--for=condition=Established', 'crd/tlsstores.traefik.io', '--timeout=60s'], { stdio: 'ignore' });
|
|
136
|
+
|
|
137
|
+
run('kubectl', ['apply', '-f', path.join(MANIFESTS_DIR, 'tls-store.yaml')]);
|
|
138
|
+
run('kubectl', ['apply', '-f', path.join(MANIFESTS_DIR, 'namespace-addons.yaml')]);
|
|
139
|
+
logger.success('TLS configured, default cert wired into Traefik');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Reverses provisionCluster() for one Main project: deletes its cluster, registry container,
|
|
143
|
+
// network, and image volume. Does NOT touch the Podman VM's insecure-registry trust (that's
|
|
144
|
+
// VM-wide, torn down once by `localctl uninstall`, not per-cluster) or *.local.test hosts/certs
|
|
145
|
+
// (shared across every cluster).
|
|
146
|
+
function teardownCluster(clusterName) {
|
|
147
|
+
if (commandExists('k3d')) {
|
|
148
|
+
logger.step(`Deleting k3d cluster '${clusterName}'...`);
|
|
149
|
+
run('k3d', ['cluster', 'delete', clusterName], { allowFail: true });
|
|
150
|
+
}
|
|
151
|
+
const detected = detectEngine();
|
|
152
|
+
if (detected) {
|
|
153
|
+
const { bin: engineBin } = detected;
|
|
154
|
+
run(engineBin, ['rm', '-f', '-v', registryNameFor(clusterName)], { stdio: 'ignore', allowFail: true });
|
|
155
|
+
run(engineBin, ['network', 'rm', `k3d-${clusterName}`], { stdio: 'ignore', allowFail: true });
|
|
156
|
+
run(engineBin, ['volume', 'rm', `k3d-${clusterName}-images`], { stdio: 'ignore', allowFail: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Throws with the exact fix command if `clusterName` isn't up yet, otherwise points kubectl at
|
|
161
|
+
// it. Called by every app-scoped command before it does anything else, so "cd in and run a
|
|
162
|
+
// command" fails with a clear instruction instead of a confusing kubectl error further down.
|
|
163
|
+
function ensureClusterUp(clusterName, profileName) {
|
|
164
|
+
if (!clusterExists(clusterName)) {
|
|
165
|
+
const fix = profileName === 'default' ? 'localctl setup' : `localctl profiles switch ${profileName}`;
|
|
166
|
+
throw new Error(`No cluster running for project "${profileName}". Run "${fix}" to start it.`);
|
|
167
|
+
}
|
|
168
|
+
run('kubectl', ['config', 'use-context', `k3d-${clusterName}`], { stdio: 'ignore' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { provisionCluster, teardownCluster, clusterExists, registryNameFor, ensureClusterUp };
|
package/src/lib/configSchema.js
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
const { z } = require('zod');
|
|
2
|
-
const { knownAddonTypes } = require('./manifestGen');
|
|
3
|
-
|
|
4
|
-
// Dependency types are discovered from cli/src/lib/addons/<type>/addon.yaml (see
|
|
5
|
-
// docs/adding-tools.md) rather than hardcoded here - dropping in a new addon folder is enough to
|
|
6
|
-
// make it usable, no code change needed.
|
|
7
|
-
const dependencySchema = z.object({
|
|
8
|
-
type: z.enum(knownAddonTypes()),
|
|
9
|
-
version: z.string().optional(),
|
|
10
|
-
database: z.string().optional(),
|
|
11
|
-
storage: z.string().optional(),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const debugSchema = z
|
|
15
|
-
.object({
|
|
16
|
-
enabled: z.boolean().default(false),
|
|
17
|
-
port: z.number().int().optional(),
|
|
18
|
-
type: z.enum(['node', 'python', 'go', 'java', 'dotnet']).optional(),
|
|
19
|
-
})
|
|
20
|
-
.default({ enabled: false });
|
|
21
|
-
|
|
22
|
-
const syncEntrySchema = z.object({
|
|
23
|
-
localPath: z.string(),
|
|
24
|
-
remotePath: z.string(),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// An extra container in the same pod as your app - the classic k8s sidecar pattern (a DB admin
|
|
28
|
-
// UI next to a database, a log/metrics shipper, a background job runner next to a web app). It
|
|
29
|
-
// shares the pod's network namespace with your app automatically - no Service needed for your app
|
|
30
|
-
// to reach it, just `localhost:<port>`. See docs/config-schema.md for a worked example.
|
|
31
|
-
const sidecarSchema = z.object({
|
|
32
|
-
name: z.string().regex(/^[a-z0-9-]+$/, 'must be lowercase alphanumeric + dashes only'),
|
|
33
|
-
image: z.string(),
|
|
34
|
-
port: z.number().int().optional(),
|
|
35
|
-
command: z.array(z.string()).optional(),
|
|
36
|
-
args: z.array(z.string()).optional(),
|
|
37
|
-
env: z.record(z.string()).default({}),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const resourceSpec = z
|
|
41
|
-
.object({
|
|
42
|
-
cpu: z.string().optional(),
|
|
43
|
-
memory: z.string().optional(),
|
|
44
|
-
})
|
|
45
|
-
.optional();
|
|
46
|
-
|
|
47
|
-
// A single probe - defaults to a plain TCP check on the app's own `port` if `path` isn't given
|
|
48
|
-
// (see buildProbe() in manifestGen.js). Applies to the main app container only.
|
|
49
|
-
const probeSchema = z.object({
|
|
50
|
-
path: z.string().optional(),
|
|
51
|
-
initialDelaySeconds: z.number().int().optional(),
|
|
52
|
-
periodSeconds: z.number().int().optional(),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const probesSchema = z
|
|
56
|
-
.object({
|
|
57
|
-
readiness: probeSchema.optional(),
|
|
58
|
-
liveness: probeSchema.optional(),
|
|
59
|
-
})
|
|
60
|
-
.default({});
|
|
61
|
-
|
|
62
|
-
const appConfigSchema = z
|
|
63
|
-
.object({
|
|
64
|
-
name: z
|
|
65
|
-
.string()
|
|
66
|
-
.regex(/^[a-z0-9-]+$/, 'must be lowercase alphanumeric + dashes only'),
|
|
67
|
-
subdomain: z.string().regex(/^[a-z0-9-]+$/).optional(),
|
|
68
|
-
namespace: z.string().optional(),
|
|
69
|
-
port: z.number().int(),
|
|
70
|
-
build: z.object({
|
|
71
|
-
context: z.string().default('.'),
|
|
72
|
-
dockerfile: z.string().default('Dockerfile'),
|
|
73
|
-
}),
|
|
74
|
-
env: z.record(z.string()).default({}),
|
|
75
|
-
// Names only - values never live here. Set with `localctl app secrets set <KEY>`, which
|
|
76
|
-
// stores the value once as a real k8s Secret and injects it via secretKeyRef, the same
|
|
77
|
-
// never-plaintext principle addon-provided credentials already follow.
|
|
78
|
-
secrets: z
|
|
79
|
-
.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/, 'must be UPPER_SNAKE_CASE'))
|
|
80
|
-
.default([]),
|
|
81
|
-
replicas: z.number().int().min(1).default(1),
|
|
82
|
-
resources: z
|
|
83
|
-
.object({ requests: resourceSpec, limits: resourceSpec })
|
|
84
|
-
.optional(),
|
|
85
|
-
debug: debugSchema,
|
|
86
|
-
probes: probesSchema,
|
|
87
|
-
dependencies: z.array(dependencySchema).default([]),
|
|
88
|
-
sidecars: z.array(sidecarSchema).default([]),
|
|
89
|
-
sync: z.array(syncEntrySchema).default([]),
|
|
90
|
-
tls: z.boolean().default(true),
|
|
91
|
-
})
|
|
92
|
-
.superRefine((cfg, ctx) => {
|
|
93
|
-
const overlap = cfg.secrets.filter((key) => key in cfg.env);
|
|
94
|
-
if (overlap.length > 0) {
|
|
95
|
-
ctx.addIssue({
|
|
96
|
-
code: z.ZodIssueCode.custom,
|
|
97
|
-
path: ['secrets'],
|
|
98
|
-
message: `${overlap.join(', ')} can't be in both "env" and "secrets" - pick one.`,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
module.exports = { appConfigSchema };
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { knownAddonTypes } = require('./manifestGen');
|
|
3
|
+
|
|
4
|
+
// Dependency types are discovered from cli/src/lib/addons/<type>/addon.yaml (see
|
|
5
|
+
// docs/adding-tools.md) rather than hardcoded here - dropping in a new addon folder is enough to
|
|
6
|
+
// make it usable, no code change needed.
|
|
7
|
+
const dependencySchema = z.object({
|
|
8
|
+
type: z.enum(knownAddonTypes()),
|
|
9
|
+
version: z.string().optional(),
|
|
10
|
+
database: z.string().optional(),
|
|
11
|
+
storage: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const debugSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
enabled: z.boolean().default(false),
|
|
17
|
+
port: z.number().int().optional(),
|
|
18
|
+
type: z.enum(['node', 'python', 'go', 'java', 'dotnet']).optional(),
|
|
19
|
+
})
|
|
20
|
+
.default({ enabled: false });
|
|
21
|
+
|
|
22
|
+
const syncEntrySchema = z.object({
|
|
23
|
+
localPath: z.string(),
|
|
24
|
+
remotePath: z.string(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// An extra container in the same pod as your app - the classic k8s sidecar pattern (a DB admin
|
|
28
|
+
// UI next to a database, a log/metrics shipper, a background job runner next to a web app). It
|
|
29
|
+
// shares the pod's network namespace with your app automatically - no Service needed for your app
|
|
30
|
+
// to reach it, just `localhost:<port>`. See docs/config-schema.md for a worked example.
|
|
31
|
+
const sidecarSchema = z.object({
|
|
32
|
+
name: z.string().regex(/^[a-z0-9-]+$/, 'must be lowercase alphanumeric + dashes only'),
|
|
33
|
+
image: z.string(),
|
|
34
|
+
port: z.number().int().optional(),
|
|
35
|
+
command: z.array(z.string()).optional(),
|
|
36
|
+
args: z.array(z.string()).optional(),
|
|
37
|
+
env: z.record(z.string()).default({}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const resourceSpec = z
|
|
41
|
+
.object({
|
|
42
|
+
cpu: z.string().optional(),
|
|
43
|
+
memory: z.string().optional(),
|
|
44
|
+
})
|
|
45
|
+
.optional();
|
|
46
|
+
|
|
47
|
+
// A single probe - defaults to a plain TCP check on the app's own `port` if `path` isn't given
|
|
48
|
+
// (see buildProbe() in manifestGen.js). Applies to the main app container only.
|
|
49
|
+
const probeSchema = z.object({
|
|
50
|
+
path: z.string().optional(),
|
|
51
|
+
initialDelaySeconds: z.number().int().optional(),
|
|
52
|
+
periodSeconds: z.number().int().optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const probesSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
readiness: probeSchema.optional(),
|
|
58
|
+
liveness: probeSchema.optional(),
|
|
59
|
+
})
|
|
60
|
+
.default({});
|
|
61
|
+
|
|
62
|
+
const appConfigSchema = z
|
|
63
|
+
.object({
|
|
64
|
+
name: z
|
|
65
|
+
.string()
|
|
66
|
+
.regex(/^[a-z0-9-]+$/, 'must be lowercase alphanumeric + dashes only'),
|
|
67
|
+
subdomain: z.string().regex(/^[a-z0-9-]+$/).optional(),
|
|
68
|
+
namespace: z.string().optional(),
|
|
69
|
+
port: z.number().int(),
|
|
70
|
+
build: z.object({
|
|
71
|
+
context: z.string().default('.'),
|
|
72
|
+
dockerfile: z.string().default('Dockerfile'),
|
|
73
|
+
}),
|
|
74
|
+
env: z.record(z.string()).default({}),
|
|
75
|
+
// Names only - values never live here. Set with `localctl app secrets set <KEY>`, which
|
|
76
|
+
// stores the value once as a real k8s Secret and injects it via secretKeyRef, the same
|
|
77
|
+
// never-plaintext principle addon-provided credentials already follow.
|
|
78
|
+
secrets: z
|
|
79
|
+
.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/, 'must be UPPER_SNAKE_CASE'))
|
|
80
|
+
.default([]),
|
|
81
|
+
replicas: z.number().int().min(1).default(1),
|
|
82
|
+
resources: z
|
|
83
|
+
.object({ requests: resourceSpec, limits: resourceSpec })
|
|
84
|
+
.optional(),
|
|
85
|
+
debug: debugSchema,
|
|
86
|
+
probes: probesSchema,
|
|
87
|
+
dependencies: z.array(dependencySchema).default([]),
|
|
88
|
+
sidecars: z.array(sidecarSchema).default([]),
|
|
89
|
+
sync: z.array(syncEntrySchema).default([]),
|
|
90
|
+
tls: z.boolean().default(true),
|
|
91
|
+
})
|
|
92
|
+
.superRefine((cfg, ctx) => {
|
|
93
|
+
const overlap = cfg.secrets.filter((key) => key in cfg.env);
|
|
94
|
+
if (overlap.length > 0) {
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: z.ZodIssueCode.custom,
|
|
97
|
+
path: ['secrets'],
|
|
98
|
+
message: `${overlap.join(', ')} can't be in both "env" and "secrets" - pick one.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
module.exports = { appConfigSchema };
|
package/src/lib/constants.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
// The "default" profile's cluster name - the single cluster this whole tool managed before
|
|
3
|
-
// multi-project support existed. Other Main-level projects get their own `local-dev-<name>`
|
|
4
|
-
// cluster (see profileStore.js).
|
|
5
|
-
DEFAULT_CLUSTER_NAME: 'local-dev',
|
|
6
|
-
DEFAULT_HTTP_PORT: 80,
|
|
7
|
-
DEFAULT_HTTPS_PORT: 443,
|
|
8
|
-
// .test (RFC 2606) is reserved for testing and guaranteed to never resolve publicly or be
|
|
9
|
-
// HSTS-preloaded. Not .dev: it's a real, live gTLD (Google-operated) that's HSTS-preloaded at
|
|
10
|
-
// the TLD level - every browser force-HTTPS's *any* .dev domain, including made-up ones, with
|
|
11
|
-
// no way to click through a self-signed cert warning, and "local.dev" itself is registered by
|
|
12
|
-
// an unrelated third party.
|
|
13
|
-
DOMAIN: 'local.test',
|
|
14
|
-
REGISTRY_HOST_PORT: '5050',
|
|
15
|
-
OBSERVABILITY_NAMESPACE: 'observability',
|
|
16
|
-
};
|
|
1
|
+
module.exports = {
|
|
2
|
+
// The "default" profile's cluster name - the single cluster this whole tool managed before
|
|
3
|
+
// multi-project support existed. Other Main-level projects get their own `local-dev-<name>`
|
|
4
|
+
// cluster (see profileStore.js).
|
|
5
|
+
DEFAULT_CLUSTER_NAME: 'local-dev',
|
|
6
|
+
DEFAULT_HTTP_PORT: 80,
|
|
7
|
+
DEFAULT_HTTPS_PORT: 443,
|
|
8
|
+
// .test (RFC 2606) is reserved for testing and guaranteed to never resolve publicly or be
|
|
9
|
+
// HSTS-preloaded. Not .dev: it's a real, live gTLD (Google-operated) that's HSTS-preloaded at
|
|
10
|
+
// the TLD level - every browser force-HTTPS's *any* .dev domain, including made-up ones, with
|
|
11
|
+
// no way to click through a self-signed cert warning, and "local.dev" itself is registered by
|
|
12
|
+
// an unrelated third party.
|
|
13
|
+
DOMAIN: 'local.test',
|
|
14
|
+
REGISTRY_HOST_PORT: '5050',
|
|
15
|
+
OBSERVABILITY_NAMESPACE: 'observability',
|
|
16
|
+
};
|