@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.
- 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/commands/secrets.js
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
const { loadConfig } = require('../lib/loadConfig');
|
|
2
|
-
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
3
|
-
const { setSecret, unsetSecret, listKeys, readAll } = require('../lib/appSecrets');
|
|
4
|
-
const { promptHidden } = require('../lib/prompt');
|
|
5
|
-
const ui = require('../lib/ui');
|
|
6
|
-
const logger = require('../lib/logger');
|
|
7
|
-
|
|
8
|
-
module.exports = (program) => {
|
|
9
|
-
const secrets = program.command('secrets').description('Manage this app\'s own secrets (API keys, tokens - not addon-generated credentials)');
|
|
10
|
-
|
|
11
|
-
secrets
|
|
12
|
-
.command('set <key> [value]')
|
|
13
|
-
.description('Set a secret value (prompts, hidden, if not given) - stored as a real k8s Secret, never in .local/config.json')
|
|
14
|
-
.action(async (key, value) => {
|
|
15
|
-
const cfg = loadConfig();
|
|
16
|
-
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
17
|
-
if (!cfg.secrets.includes(key)) {
|
|
18
|
-
logger.warn(`"${key}" isn't listed in .local/config.json's "secrets" array - it won't be injected into the app until you add it there too.`);
|
|
19
|
-
}
|
|
20
|
-
const finalValue = value || (await promptHidden(`Value for ${key}`));
|
|
21
|
-
if (!finalValue) throw new Error('No value given.');
|
|
22
|
-
setSecret(cfg, key, finalValue);
|
|
23
|
-
logger.success(`Set "${key}". Run "localctl app up"/"localctl app reload" to pick it up.`);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
secrets
|
|
27
|
-
.command('unset <key>')
|
|
28
|
-
.description('Remove a secret value')
|
|
29
|
-
.action((key) => {
|
|
30
|
-
const cfg = loadConfig();
|
|
31
|
-
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
32
|
-
const removed = unsetSecret(cfg, key);
|
|
33
|
-
if (removed) logger.success(`Removed "${key}".`);
|
|
34
|
-
else logger.info(`"${key}" wasn't set.`);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
secrets
|
|
38
|
-
.command('list')
|
|
39
|
-
.description('List which secret keys are set (not their values)')
|
|
40
|
-
.action(() => {
|
|
41
|
-
const cfg = loadConfig();
|
|
42
|
-
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
43
|
-
const set = new Set(listKeys(cfg));
|
|
44
|
-
if (cfg.secrets.length === 0 && set.size === 0) {
|
|
45
|
-
logger.info('No secrets declared or set for this app.');
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const rows = [...new Set([...cfg.secrets, ...set])].map((key) => [
|
|
49
|
-
key,
|
|
50
|
-
set.has(key) ? ui.ok('set') : ui.warn('missing'),
|
|
51
|
-
cfg.secrets.includes(key) ? '' : ui.dim('(not in config.json)'),
|
|
52
|
-
]);
|
|
53
|
-
ui.printTable(['KEY', 'STATUS', ''], rows);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
secrets
|
|
57
|
-
.command('show [key]')
|
|
58
|
-
.description('Reveal actual secret value(s) - use with care, prints them to your terminal')
|
|
59
|
-
.action((key) => {
|
|
60
|
-
const cfg = loadConfig();
|
|
61
|
-
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
62
|
-
const data = readAll(cfg);
|
|
63
|
-
if (key) {
|
|
64
|
-
if (!(key in data)) throw new Error(`"${key}" isn't set. Run "localctl app secrets set ${key}".`);
|
|
65
|
-
console.log(data[key]);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (Object.keys(data).length === 0) {
|
|
69
|
-
logger.info('No secrets set for this app.');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
logger.warn('Revealing real secret values - avoid leaving these in your terminal scrollback/screen share.');
|
|
73
|
-
Object.entries(data).forEach(([k, v]) => console.log(`${k}=${v}`));
|
|
74
|
-
});
|
|
75
|
-
};
|
|
1
|
+
const { loadConfig } = require('../lib/loadConfig');
|
|
2
|
+
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
3
|
+
const { setSecret, unsetSecret, listKeys, readAll } = require('../lib/appSecrets');
|
|
4
|
+
const { promptHidden } = require('../lib/prompt');
|
|
5
|
+
const ui = require('../lib/ui');
|
|
6
|
+
const logger = require('../lib/logger');
|
|
7
|
+
|
|
8
|
+
module.exports = (program) => {
|
|
9
|
+
const secrets = program.command('secrets').description('Manage this app\'s own secrets (API keys, tokens - not addon-generated credentials)');
|
|
10
|
+
|
|
11
|
+
secrets
|
|
12
|
+
.command('set <key> [value]')
|
|
13
|
+
.description('Set a secret value (prompts, hidden, if not given) - stored as a real k8s Secret, never in .local/config.json')
|
|
14
|
+
.action(async (key, value) => {
|
|
15
|
+
const cfg = loadConfig();
|
|
16
|
+
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
17
|
+
if (!cfg.secrets.includes(key)) {
|
|
18
|
+
logger.warn(`"${key}" isn't listed in .local/config.json's "secrets" array - it won't be injected into the app until you add it there too.`);
|
|
19
|
+
}
|
|
20
|
+
const finalValue = value || (await promptHidden(`Value for ${key}`));
|
|
21
|
+
if (!finalValue) throw new Error('No value given.');
|
|
22
|
+
setSecret(cfg, key, finalValue);
|
|
23
|
+
logger.success(`Set "${key}". Run "localctl app up"/"localctl app reload" to pick it up.`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
secrets
|
|
27
|
+
.command('unset <key>')
|
|
28
|
+
.description('Remove a secret value')
|
|
29
|
+
.action((key) => {
|
|
30
|
+
const cfg = loadConfig();
|
|
31
|
+
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
32
|
+
const removed = unsetSecret(cfg, key);
|
|
33
|
+
if (removed) logger.success(`Removed "${key}".`);
|
|
34
|
+
else logger.info(`"${key}" wasn't set.`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
secrets
|
|
38
|
+
.command('list')
|
|
39
|
+
.description('List which secret keys are set (not their values)')
|
|
40
|
+
.action(() => {
|
|
41
|
+
const cfg = loadConfig();
|
|
42
|
+
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
43
|
+
const set = new Set(listKeys(cfg));
|
|
44
|
+
if (cfg.secrets.length === 0 && set.size === 0) {
|
|
45
|
+
logger.info('No secrets declared or set for this app.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const rows = [...new Set([...cfg.secrets, ...set])].map((key) => [
|
|
49
|
+
key,
|
|
50
|
+
set.has(key) ? ui.ok('set') : ui.warn('missing'),
|
|
51
|
+
cfg.secrets.includes(key) ? '' : ui.dim('(not in config.json)'),
|
|
52
|
+
]);
|
|
53
|
+
ui.printTable(['KEY', 'STATUS', ''], rows);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
secrets
|
|
57
|
+
.command('show [key]')
|
|
58
|
+
.description('Reveal actual secret value(s) - use with care, prints them to your terminal')
|
|
59
|
+
.action((key) => {
|
|
60
|
+
const cfg = loadConfig();
|
|
61
|
+
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
62
|
+
const data = readAll(cfg);
|
|
63
|
+
if (key) {
|
|
64
|
+
if (!(key in data)) throw new Error(`"${key}" isn't set. Run "localctl app secrets set ${key}".`);
|
|
65
|
+
console.log(data[key]);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (Object.keys(data).length === 0) {
|
|
69
|
+
logger.info('No secrets set for this app.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
logger.warn('Revealing real secret values - avoid leaving these in your terminal scrollback/screen share.');
|
|
73
|
+
Object.entries(data).forEach(([k, v]) => console.log(`${k}=${v}`));
|
|
74
|
+
});
|
|
75
|
+
};
|
package/src/commands/setup.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
const { provisionCluster } = require('../lib/clusterProvision');
|
|
2
|
-
const { syncHosts } = require('../lib/hostsFile');
|
|
3
|
-
const { setActive } = require('../lib/profileStore');
|
|
4
|
-
const { DEFAULT_CLUSTER_NAME, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, DOMAIN, REGISTRY_HOST_PORT } = require('../lib/constants');
|
|
5
|
-
const logger = require('../lib/logger');
|
|
6
|
-
|
|
7
|
-
function setup() {
|
|
8
|
-
provisionCluster({
|
|
9
|
-
clusterName: DEFAULT_CLUSTER_NAME,
|
|
10
|
-
httpPort: DEFAULT_HTTP_PORT,
|
|
11
|
-
httpsPort: DEFAULT_HTTPS_PORT,
|
|
12
|
-
registryPort: REGISTRY_HOST_PORT,
|
|
13
|
-
});
|
|
14
|
-
setActive('default');
|
|
15
|
-
|
|
16
|
-
// Hosts file: seed it now (prompts for sudo here, once, instead of surprising you mid
|
|
17
|
-
// `localctl app up` later). Only adds `local.test` itself at this point - per-app subdomains get
|
|
18
|
-
// added the same way when you run `localctl app new`/`up` for each app.
|
|
19
|
-
logger.step('Syncing hosts file...');
|
|
20
|
-
syncHosts();
|
|
21
|
-
|
|
22
|
-
console.log();
|
|
23
|
-
logger.success('Setup complete.');
|
|
24
|
-
console.log('Next steps:');
|
|
25
|
-
console.log(' 1. cd into an app repo');
|
|
26
|
-
console.log(' 2. localctl app new');
|
|
27
|
-
console.log(' 3. localctl app up');
|
|
28
|
-
console.log(` 4. open https://<subdomain>.${DOMAIN}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
module.exports = (program) => {
|
|
32
|
-
program
|
|
33
|
-
.command('setup')
|
|
34
|
-
.description('One-click local infra setup for the default project: container engine, tools, k3d cluster, TLS, hosts')
|
|
35
|
-
.action(setup);
|
|
36
|
-
};
|
|
1
|
+
const { provisionCluster } = require('../lib/clusterProvision');
|
|
2
|
+
const { syncHosts } = require('../lib/hostsFile');
|
|
3
|
+
const { setActive } = require('../lib/profileStore');
|
|
4
|
+
const { DEFAULT_CLUSTER_NAME, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, DOMAIN, REGISTRY_HOST_PORT } = require('../lib/constants');
|
|
5
|
+
const logger = require('../lib/logger');
|
|
6
|
+
|
|
7
|
+
function setup() {
|
|
8
|
+
provisionCluster({
|
|
9
|
+
clusterName: DEFAULT_CLUSTER_NAME,
|
|
10
|
+
httpPort: DEFAULT_HTTP_PORT,
|
|
11
|
+
httpsPort: DEFAULT_HTTPS_PORT,
|
|
12
|
+
registryPort: REGISTRY_HOST_PORT,
|
|
13
|
+
});
|
|
14
|
+
setActive('default');
|
|
15
|
+
|
|
16
|
+
// Hosts file: seed it now (prompts for sudo here, once, instead of surprising you mid
|
|
17
|
+
// `localctl app up` later). Only adds `local.test` itself at this point - per-app subdomains get
|
|
18
|
+
// added the same way when you run `localctl app new`/`up` for each app.
|
|
19
|
+
logger.step('Syncing hosts file...');
|
|
20
|
+
syncHosts();
|
|
21
|
+
|
|
22
|
+
console.log();
|
|
23
|
+
logger.success('Setup complete.');
|
|
24
|
+
console.log('Next steps:');
|
|
25
|
+
console.log(' 1. cd into an app repo');
|
|
26
|
+
console.log(' 2. localctl app new');
|
|
27
|
+
console.log(' 3. localctl app up');
|
|
28
|
+
console.log(` 4. open https://<subdomain>.${DOMAIN}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = (program) => {
|
|
32
|
+
program
|
|
33
|
+
.command('setup')
|
|
34
|
+
.description('One-click local infra setup for the default project: container engine, tools, k3d cluster, TLS, hosts')
|
|
35
|
+
.action(setup);
|
|
36
|
+
};
|
package/src/commands/status.js
CHANGED
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { isRunning } = require('../lib/exec');
|
|
3
|
-
const { runFiles, CONFIG_PATH } = require('../lib/paths');
|
|
4
|
-
const { getList, workloadsWithPrefix } = require('../lib/workloadReadiness');
|
|
5
|
-
const { loadConfig } = require('../lib/loadConfig');
|
|
6
|
-
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
7
|
-
const { getActive, clusterNameFor } = require('../lib/profileStore');
|
|
8
|
-
const ui = require('../lib/ui');
|
|
9
|
-
const logger = require('../lib/logger');
|
|
10
|
-
|
|
11
|
-
function key(obj) {
|
|
12
|
-
return `${obj.metadata.namespace}/${obj.metadata.name}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function collect() {
|
|
16
|
-
const deployments = getList(['get', 'deployment', '-A', '-l', 'localctl.dev/managed=true']);
|
|
17
|
-
const ingresses = getList(['get', 'ingress', '-A', '-l', 'localctl.dev/managed=true']);
|
|
18
|
-
const services = getList(['get', 'service', '-A', '-l', 'localctl.dev/managed=true']);
|
|
19
|
-
return {
|
|
20
|
-
deployments,
|
|
21
|
-
ingressByKey: new Map(ingresses.map((i) => [key(i), i])),
|
|
22
|
-
serviceByKey: new Map(services.map((s) => [key(s), s])),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function devLoop(name) {
|
|
27
|
-
const { pidFile } = runFiles(name);
|
|
28
|
-
if (!fs.existsSync(pidFile)) return ui.dim('-');
|
|
29
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
|
|
30
|
-
if (Number.isFinite(pid) && isRunning(pid)) return ui.ok(`running (pid ${pid})`);
|
|
31
|
-
return ui.warn('stopped');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function readyLabel(dep) {
|
|
35
|
-
return ui.readyLabel(dep.status.readyReplicas || 0, dep.spec.replicas);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function urlFor(dep, ingressByKey) {
|
|
39
|
-
const ing = ingressByKey.get(key(dep));
|
|
40
|
-
const rule = ing && ing.spec.rules && ing.spec.rules[0];
|
|
41
|
-
if (!rule) return ui.dim('-');
|
|
42
|
-
const scheme = ing.spec.tls && ing.spec.tls.length ? 'https' : 'http';
|
|
43
|
-
return ui.url(`${scheme}://${rule.host}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function debugPortFor(dep, serviceByKey) {
|
|
47
|
-
const svc = serviceByKey.get(key(dep));
|
|
48
|
-
const port = svc && svc.spec.ports.find((p) => p.name === 'debug');
|
|
49
|
-
return port ? String(port.port) : null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function dependenciesFor(dep) {
|
|
53
|
-
// Real Helm charts pick whichever workload kind fits them - not every addon is a plain
|
|
54
|
-
// Deployment like the app itself always is (see lib/workloadReadiness.js).
|
|
55
|
-
const prefix = `${dep.metadata.name}-`;
|
|
56
|
-
return workloadsWithPrefix(dep.metadata.namespace, prefix)
|
|
57
|
-
.map((w) => ({ name: w.name, ready: ui.readyLabel(w.ready, w.desired) }));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function printSingle(dep, ingressByKey, serviceByKey) {
|
|
61
|
-
const debugPort = debugPortFor(dep, serviceByKey);
|
|
62
|
-
console.log(`${ui.bold(dep.metadata.name)}`);
|
|
63
|
-
console.log(` namespace: ${dep.metadata.namespace}`);
|
|
64
|
-
console.log(` ready: ${readyLabel(dep)}`);
|
|
65
|
-
console.log(` url: ${urlFor(dep, ingressByKey)}`);
|
|
66
|
-
if (debugPort) console.log(` debug: localhost:${debugPort}`);
|
|
67
|
-
console.log(` dev loop: ${devLoop(dep.metadata.name)}`);
|
|
68
|
-
|
|
69
|
-
const deps = dependenciesFor(dep);
|
|
70
|
-
if (deps.length > 0) {
|
|
71
|
-
console.log(' dependencies:');
|
|
72
|
-
deps.forEach((d) => console.log(` ${d.name} ${d.ready}`));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function printAll(deployments, ingressByKey, serviceByKey) {
|
|
77
|
-
const rows = deployments.map((dep) => [
|
|
78
|
-
dep.metadata.name,
|
|
79
|
-
dep.metadata.namespace,
|
|
80
|
-
readyLabel(dep),
|
|
81
|
-
urlFor(dep, ingressByKey),
|
|
82
|
-
devLoop(dep.metadata.name),
|
|
83
|
-
]);
|
|
84
|
-
ui.printTable(['APP', 'NAMESPACE', 'READY', 'URL', 'DEV LOOP'], rows);
|
|
85
|
-
|
|
86
|
-
if (!deployments.some((dep) => devLoop(dep.metadata.name).includes('running'))) {
|
|
87
|
-
console.log('');
|
|
88
|
-
logger.info('Dev loop not running locally for any app - "localctl app up" from an app\'s directory to start one.');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = (program) => {
|
|
93
|
-
program
|
|
94
|
-
.command('status')
|
|
95
|
-
.option('-a, --app <name>', 'show status for one app by name, in the currently active project\'s cluster')
|
|
96
|
-
.option('-A, --all', 'show every app in the currently active project\'s cluster, even from inside an app repo')
|
|
97
|
-
.description('Show every localctl-managed app in a cluster: readiness, URL, local dev-loop state')
|
|
98
|
-
.action((opts) => {
|
|
99
|
-
let targetName = opts.app;
|
|
100
|
-
let targetNamespace = null;
|
|
101
|
-
|
|
102
|
-
// Directory-resolved single-app view: figures out which project (and so which cluster and
|
|
103
|
-
// namespace/subdomain prefix) this app belongs to on its own - see loadConfig.js.
|
|
104
|
-
if (!targetName && !opts.all && fs.existsSync(CONFIG_PATH)) {
|
|
105
|
-
const cfg = loadConfig();
|
|
106
|
-
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
107
|
-
targetName = cfg.name;
|
|
108
|
-
targetNamespace = cfg.namespace;
|
|
109
|
-
} else {
|
|
110
|
-
// No directory context (elsewhere, -a, or -A): fall back to whichever project is
|
|
111
|
-
// currently active (see "localctl profiles switch").
|
|
112
|
-
const active = getActive();
|
|
113
|
-
ensureClusterUp(clusterNameFor(active), active);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const { deployments, ingressByKey, serviceByKey } = collect();
|
|
117
|
-
if (deployments.length === 0) {
|
|
118
|
-
logger.info('No localctl-managed apps found in the cluster.');
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (targetName) {
|
|
123
|
-
const dep = deployments.find(
|
|
124
|
-
(d) => d.metadata.name === targetName && (!targetNamespace || d.metadata.namespace === targetNamespace),
|
|
125
|
-
);
|
|
126
|
-
if (!dep) {
|
|
127
|
-
logger.warn(`No deployed app named "${targetName}" found in the cluster.`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
printSingle(dep, ingressByKey, serviceByKey);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
printAll(deployments, ingressByKey, serviceByKey);
|
|
135
|
-
});
|
|
136
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { isRunning } = require('../lib/exec');
|
|
3
|
+
const { runFiles, CONFIG_PATH } = require('../lib/paths');
|
|
4
|
+
const { getList, workloadsWithPrefix } = require('../lib/workloadReadiness');
|
|
5
|
+
const { loadConfig } = require('../lib/loadConfig');
|
|
6
|
+
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
7
|
+
const { getActive, clusterNameFor } = require('../lib/profileStore');
|
|
8
|
+
const ui = require('../lib/ui');
|
|
9
|
+
const logger = require('../lib/logger');
|
|
10
|
+
|
|
11
|
+
function key(obj) {
|
|
12
|
+
return `${obj.metadata.namespace}/${obj.metadata.name}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function collect() {
|
|
16
|
+
const deployments = getList(['get', 'deployment', '-A', '-l', 'localctl.dev/managed=true']);
|
|
17
|
+
const ingresses = getList(['get', 'ingress', '-A', '-l', 'localctl.dev/managed=true']);
|
|
18
|
+
const services = getList(['get', 'service', '-A', '-l', 'localctl.dev/managed=true']);
|
|
19
|
+
return {
|
|
20
|
+
deployments,
|
|
21
|
+
ingressByKey: new Map(ingresses.map((i) => [key(i), i])),
|
|
22
|
+
serviceByKey: new Map(services.map((s) => [key(s), s])),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function devLoop(name) {
|
|
27
|
+
const { pidFile } = runFiles(name);
|
|
28
|
+
if (!fs.existsSync(pidFile)) return ui.dim('-');
|
|
29
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
|
|
30
|
+
if (Number.isFinite(pid) && isRunning(pid)) return ui.ok(`running (pid ${pid})`);
|
|
31
|
+
return ui.warn('stopped');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readyLabel(dep) {
|
|
35
|
+
return ui.readyLabel(dep.status.readyReplicas || 0, dep.spec.replicas);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function urlFor(dep, ingressByKey) {
|
|
39
|
+
const ing = ingressByKey.get(key(dep));
|
|
40
|
+
const rule = ing && ing.spec.rules && ing.spec.rules[0];
|
|
41
|
+
if (!rule) return ui.dim('-');
|
|
42
|
+
const scheme = ing.spec.tls && ing.spec.tls.length ? 'https' : 'http';
|
|
43
|
+
return ui.url(`${scheme}://${rule.host}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function debugPortFor(dep, serviceByKey) {
|
|
47
|
+
const svc = serviceByKey.get(key(dep));
|
|
48
|
+
const port = svc && svc.spec.ports.find((p) => p.name === 'debug');
|
|
49
|
+
return port ? String(port.port) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dependenciesFor(dep) {
|
|
53
|
+
// Real Helm charts pick whichever workload kind fits them - not every addon is a plain
|
|
54
|
+
// Deployment like the app itself always is (see lib/workloadReadiness.js).
|
|
55
|
+
const prefix = `${dep.metadata.name}-`;
|
|
56
|
+
return workloadsWithPrefix(dep.metadata.namespace, prefix)
|
|
57
|
+
.map((w) => ({ name: w.name, ready: ui.readyLabel(w.ready, w.desired) }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printSingle(dep, ingressByKey, serviceByKey) {
|
|
61
|
+
const debugPort = debugPortFor(dep, serviceByKey);
|
|
62
|
+
console.log(`${ui.bold(dep.metadata.name)}`);
|
|
63
|
+
console.log(` namespace: ${dep.metadata.namespace}`);
|
|
64
|
+
console.log(` ready: ${readyLabel(dep)}`);
|
|
65
|
+
console.log(` url: ${urlFor(dep, ingressByKey)}`);
|
|
66
|
+
if (debugPort) console.log(` debug: localhost:${debugPort}`);
|
|
67
|
+
console.log(` dev loop: ${devLoop(dep.metadata.name)}`);
|
|
68
|
+
|
|
69
|
+
const deps = dependenciesFor(dep);
|
|
70
|
+
if (deps.length > 0) {
|
|
71
|
+
console.log(' dependencies:');
|
|
72
|
+
deps.forEach((d) => console.log(` ${d.name} ${d.ready}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printAll(deployments, ingressByKey, serviceByKey) {
|
|
77
|
+
const rows = deployments.map((dep) => [
|
|
78
|
+
dep.metadata.name,
|
|
79
|
+
dep.metadata.namespace,
|
|
80
|
+
readyLabel(dep),
|
|
81
|
+
urlFor(dep, ingressByKey),
|
|
82
|
+
devLoop(dep.metadata.name),
|
|
83
|
+
]);
|
|
84
|
+
ui.printTable(['APP', 'NAMESPACE', 'READY', 'URL', 'DEV LOOP'], rows);
|
|
85
|
+
|
|
86
|
+
if (!deployments.some((dep) => devLoop(dep.metadata.name).includes('running'))) {
|
|
87
|
+
console.log('');
|
|
88
|
+
logger.info('Dev loop not running locally for any app - "localctl app up" from an app\'s directory to start one.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = (program) => {
|
|
93
|
+
program
|
|
94
|
+
.command('status')
|
|
95
|
+
.option('-a, --app <name>', 'show status for one app by name, in the currently active project\'s cluster')
|
|
96
|
+
.option('-A, --all', 'show every app in the currently active project\'s cluster, even from inside an app repo')
|
|
97
|
+
.description('Show every localctl-managed app in a cluster: readiness, URL, local dev-loop state')
|
|
98
|
+
.action((opts) => {
|
|
99
|
+
let targetName = opts.app;
|
|
100
|
+
let targetNamespace = null;
|
|
101
|
+
|
|
102
|
+
// Directory-resolved single-app view: figures out which project (and so which cluster and
|
|
103
|
+
// namespace/subdomain prefix) this app belongs to on its own - see loadConfig.js.
|
|
104
|
+
if (!targetName && !opts.all && fs.existsSync(CONFIG_PATH)) {
|
|
105
|
+
const cfg = loadConfig();
|
|
106
|
+
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
107
|
+
targetName = cfg.name;
|
|
108
|
+
targetNamespace = cfg.namespace;
|
|
109
|
+
} else {
|
|
110
|
+
// No directory context (elsewhere, -a, or -A): fall back to whichever project is
|
|
111
|
+
// currently active (see "localctl profiles switch").
|
|
112
|
+
const active = getActive();
|
|
113
|
+
ensureClusterUp(clusterNameFor(active), active);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { deployments, ingressByKey, serviceByKey } = collect();
|
|
117
|
+
if (deployments.length === 0) {
|
|
118
|
+
logger.info('No localctl-managed apps found in the cluster.');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (targetName) {
|
|
123
|
+
const dep = deployments.find(
|
|
124
|
+
(d) => d.metadata.name === targetName && (!targetNamespace || d.metadata.namespace === targetNamespace),
|
|
125
|
+
);
|
|
126
|
+
if (!dep) {
|
|
127
|
+
logger.warn(`No deployed app named "${targetName}" found in the cluster.`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
printSingle(dep, ingressByKey, serviceByKey);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
printAll(deployments, ingressByKey, serviceByKey);
|
|
135
|
+
});
|
|
136
|
+
};
|