@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.
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,39 +1,39 @@
1
- # Postgres addon - deploys the real Bitnami PostgreSQL chart, configured for local dev.
2
- # See docs/adding-tools.md for the full addon.yaml reference.
3
-
4
- chart:
5
- repo: https://charts.bitnami.com/bitnami
6
- name: postgresql
7
- version: "18.12.2"
8
-
9
- # Generates a random value once per key, stored as a real k8s Secret named "<release>-secret" -
10
- # never written to disk, never in this file. {{secretName}} below refers to that Secret's name.
11
- secrets:
12
- password:
13
- length: 20
14
-
15
- # Plain env vars injected into the app's own container automatically.
16
- env:
17
- POSTGRES_HOST: "{{serviceHost}}"
18
- POSTGRES_PORT: "5432"
19
- POSTGRES_DB: "{{database}}"
20
- POSTGRES_USER: "localdev"
21
-
22
- # Unlike `env`, these become a real secretKeyRef - the password itself never passes through a
23
- # plaintext value anywhere in this pipeline.
24
- envFromSecret:
25
- POSTGRES_PASSWORD:
26
- secret: "{{secretName}}"
27
- key: password
28
-
29
- # Passed to `helm template` as -f values.yaml. {{...}} is substituted first: {{database}}/
30
- # {{storage}}/{{version}} come from the app's own .local/config.json dependency entry (with
31
- # fallback defaults), {{secretName}} from the generated Secret above.
32
- values: |
33
- auth:
34
- username: localdev
35
- database: "{{database}}"
36
- existingSecret: "{{secretName}}"
37
- primary:
38
- persistence:
39
- size: "{{storage}}"
1
+ # Postgres addon - deploys the real Bitnami PostgreSQL chart, configured for local dev.
2
+ # See docs/adding-tools.md for the full addon.yaml reference.
3
+
4
+ chart:
5
+ repo: https://charts.bitnami.com/bitnami
6
+ name: postgresql
7
+ version: "18.12.2"
8
+
9
+ # Generates a random value once per key, stored as a real k8s Secret named "<release>-secret" -
10
+ # never written to disk, never in this file. {{secretName}} below refers to that Secret's name.
11
+ secrets:
12
+ password:
13
+ length: 20
14
+
15
+ # Plain env vars injected into the app's own container automatically.
16
+ env:
17
+ POSTGRES_HOST: "{{serviceHost}}"
18
+ POSTGRES_PORT: "5432"
19
+ POSTGRES_DB: "{{database}}"
20
+ POSTGRES_USER: "localdev"
21
+
22
+ # Unlike `env`, these become a real secretKeyRef - the password itself never passes through a
23
+ # plaintext value anywhere in this pipeline.
24
+ envFromSecret:
25
+ POSTGRES_PASSWORD:
26
+ secret: "{{secretName}}"
27
+ key: password
28
+
29
+ # Passed to `helm template` as -f values.yaml. {{...}} is substituted first: {{database}}/
30
+ # {{storage}}/{{version}} come from the app's own .local/config.json dependency entry (with
31
+ # fallback defaults), {{secretName}} from the generated Secret above.
32
+ values: |
33
+ auth:
34
+ username: localdev
35
+ database: "{{database}}"
36
+ existingSecret: "{{secretName}}"
37
+ primary:
38
+ persistence:
39
+ size: "{{storage}}"
@@ -1,29 +1,29 @@
1
- # Redis addon - deploys the real Bitnami Redis chart, configured for local dev (standalone, no
2
- # replicas). See docs/adding-tools.md for the full addon.yaml reference.
3
-
4
- chart:
5
- repo: https://charts.bitnami.com/bitnami
6
- name: redis
7
- version: "28.2.4"
8
-
9
- secrets:
10
- password:
11
- length: 20
12
-
13
- env:
14
- REDIS_HOST: "{{serviceHost}}"
15
- REDIS_PORT: "6379"
16
-
17
- envFromSecret:
18
- REDIS_PASSWORD:
19
- secret: "{{secretName}}"
20
- key: password
21
-
22
- values: |
23
- architecture: standalone
24
- auth:
25
- existingSecret: "{{secretName}}"
26
- existingSecretPasswordKey: password
27
- master:
28
- persistence:
29
- size: "{{storage}}"
1
+ # Redis addon - deploys the real Bitnami Redis chart, configured for local dev (standalone, no
2
+ # replicas). See docs/adding-tools.md for the full addon.yaml reference.
3
+
4
+ chart:
5
+ repo: https://charts.bitnami.com/bitnami
6
+ name: redis
7
+ version: "28.2.4"
8
+
9
+ secrets:
10
+ password:
11
+ length: 20
12
+
13
+ env:
14
+ REDIS_HOST: "{{serviceHost}}"
15
+ REDIS_PORT: "6379"
16
+
17
+ envFromSecret:
18
+ REDIS_PASSWORD:
19
+ secret: "{{secretName}}"
20
+ key: password
21
+
22
+ values: |
23
+ architecture: standalone
24
+ auth:
25
+ existingSecret: "{{secretName}}"
26
+ existingSecretPasswordKey: password
27
+ master:
28
+ persistence:
29
+ size: "{{storage}}"
@@ -1,60 +1,60 @@
1
- const { run, capture } = require('./exec');
2
-
3
- // One Secret per app, holding whatever keys `.local/config.json`'s "secrets" array declares.
4
- // Unlike addon-generated secrets (random values, never seen by you), these are values *you*
5
- // supply - an API key, a third-party token - so there's no "generate once" step: you set them
6
- // with `localctl app secrets set`, and manifestGen.js just references them via secretKeyRef.
7
- function secretNameFor(cfg) {
8
- return `${cfg.name}-secrets`;
9
- }
10
-
11
- function ensureNamespace(namespace) {
12
- if (capture('kubectl', ['get', 'namespace', namespace]).status !== 0) {
13
- run('kubectl', ['create', 'namespace', namespace], { stdio: 'ignore', allowFail: true });
14
- }
15
- }
16
-
17
- function readAll(cfg) {
18
- const res = capture('kubectl', ['get', 'secret', secretNameFor(cfg), '-n', cfg.namespace, '-o', 'json']);
19
- if (res.status !== 0) return {};
20
- const data = JSON.parse(res.stdout).data || {};
21
- return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, Buffer.from(v, 'base64').toString('utf8')]));
22
- }
23
-
24
- // Rewrites the whole Secret from a plain key->value map. `apply` (not `create`) makes this
25
- // idempotent - a re-run with the same data is a no-op, a changed one updates in place.
26
- function writeAll(cfg, data) {
27
- ensureNamespace(cfg.namespace);
28
- const doc = {
29
- apiVersion: 'v1',
30
- kind: 'Secret',
31
- metadata: { name: secretNameFor(cfg), namespace: cfg.namespace },
32
- type: 'Opaque',
33
- stringData: data,
34
- };
35
- run('kubectl', ['apply', '-f', '-'], { stdio: ['pipe', 'inherit', 'inherit'], input: JSON.stringify(doc) });
36
- }
37
-
38
- function setSecret(cfg, key, value) {
39
- const data = readAll(cfg);
40
- data[key] = value;
41
- writeAll(cfg, data);
42
- }
43
-
44
- function unsetSecret(cfg, key) {
45
- const data = readAll(cfg);
46
- if (!(key in data)) return false;
47
- delete data[key];
48
- if (Object.keys(data).length === 0) {
49
- run('kubectl', ['delete', 'secret', secretNameFor(cfg), '-n', cfg.namespace, '--ignore-not-found=true'], { stdio: 'ignore' });
50
- } else {
51
- writeAll(cfg, data);
52
- }
53
- return true;
54
- }
55
-
56
- function listKeys(cfg) {
57
- return Object.keys(readAll(cfg));
58
- }
59
-
60
- module.exports = { secretNameFor, setSecret, unsetSecret, listKeys, readAll };
1
+ const { run, capture } = require('./exec');
2
+
3
+ // One Secret per app, holding whatever keys `.local/config.json`'s "secrets" array declares.
4
+ // Unlike addon-generated secrets (random values, never seen by you), these are values *you*
5
+ // supply - an API key, a third-party token - so there's no "generate once" step: you set them
6
+ // with `localctl app secrets set`, and manifestGen.js just references them via secretKeyRef.
7
+ function secretNameFor(cfg) {
8
+ return `${cfg.name}-secrets`;
9
+ }
10
+
11
+ function ensureNamespace(namespace) {
12
+ if (capture('kubectl', ['get', 'namespace', namespace]).status !== 0) {
13
+ run('kubectl', ['create', 'namespace', namespace], { stdio: 'ignore', allowFail: true });
14
+ }
15
+ }
16
+
17
+ function readAll(cfg) {
18
+ const res = capture('kubectl', ['get', 'secret', secretNameFor(cfg), '-n', cfg.namespace, '-o', 'json']);
19
+ if (res.status !== 0) return {};
20
+ const data = JSON.parse(res.stdout).data || {};
21
+ return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, Buffer.from(v, 'base64').toString('utf8')]));
22
+ }
23
+
24
+ // Rewrites the whole Secret from a plain key->value map. `apply` (not `create`) makes this
25
+ // idempotent - a re-run with the same data is a no-op, a changed one updates in place.
26
+ function writeAll(cfg, data) {
27
+ ensureNamespace(cfg.namespace);
28
+ const doc = {
29
+ apiVersion: 'v1',
30
+ kind: 'Secret',
31
+ metadata: { name: secretNameFor(cfg), namespace: cfg.namespace },
32
+ type: 'Opaque',
33
+ stringData: data,
34
+ };
35
+ run('kubectl', ['apply', '-f', '-'], { stdio: ['pipe', 'inherit', 'inherit'], input: JSON.stringify(doc) });
36
+ }
37
+
38
+ function setSecret(cfg, key, value) {
39
+ const data = readAll(cfg);
40
+ data[key] = value;
41
+ writeAll(cfg, data);
42
+ }
43
+
44
+ function unsetSecret(cfg, key) {
45
+ const data = readAll(cfg);
46
+ if (!(key in data)) return false;
47
+ delete data[key];
48
+ if (Object.keys(data).length === 0) {
49
+ run('kubectl', ['delete', 'secret', secretNameFor(cfg), '-n', cfg.namespace, '--ignore-not-found=true'], { stdio: 'ignore' });
50
+ } else {
51
+ writeAll(cfg, data);
52
+ }
53
+ return true;
54
+ }
55
+
56
+ function listKeys(cfg) {
57
+ return Object.keys(readAll(cfg));
58
+ }
59
+
60
+ module.exports = { secretNameFor, setSecret, unsetSecret, listKeys, readAll };
@@ -1,59 +1,59 @@
1
- const { run } = require('./exec');
2
- const { loadAddon } = require('./addonLoader');
3
- const { findAddonDir, listAddonNames } = require('./addonDirs');
4
- const { getList, workloadsWithPrefix } = require('./workloadReadiness');
5
- const { DOMAIN, OBSERVABILITY_NAMESPACE } = require('./constants');
6
- const ui = require('./ui');
7
-
8
- // Cluster-wide addons live at <addons-dir>/cluster/<name>/addon.yaml - your own addons dir
9
- // (~/.localctl/addons, or $LOCALCTL_ADDONS_DIR) is searched before the ones bundled with this
10
- // CLI, and a name there overrides a built-in one of the same name. See docs/adding-tools.md.
11
- // Shared by `localctl addons` and `localctl profiles new`'s "carry over which addons?" wizard.
12
- function addonDirFor(name) {
13
- const dir = findAddonDir(name, 'cluster');
14
- if (!dir) {
15
- throw new Error(`Unknown addon "${name}". Known: ${listAddonNames('cluster').join(', ')}.`);
16
- }
17
- return dir;
18
- }
19
-
20
- function renderClusterAddon(name) {
21
- const { manifestYaml } = loadAddon(addonDirFor(name), {
22
- release: name,
23
- namespace: OBSERVABILITY_NAMESPACE,
24
- addonName: name,
25
- extraVars: { domain: DOMAIN },
26
- });
27
- return manifestYaml;
28
- }
29
-
30
- function enableClusterAddon(name) {
31
- run('kubectl', ['apply', '-f', '-'], { stdio: ['pipe', 'inherit', 'inherit'], input: renderClusterAddon(name) });
32
- }
33
-
34
- function disableClusterAddon(name) {
35
- run('kubectl', ['delete', '-f', '-', '--ignore-not-found=true'], { stdio: ['pipe', 'inherit', 'inherit'], input: renderClusterAddon(name) });
36
- }
37
-
38
- function readyLabel(workloads) {
39
- if (workloads.length === 0) return ui.dim('-');
40
- const ready = workloads.reduce((sum, w) => sum + w.ready, 0);
41
- const desired = workloads.reduce((sum, w) => sum + w.desired, 0);
42
- return ui.readyLabel(ready, desired);
43
- }
44
-
45
- function clusterAddonInstances() {
46
- const records = getList(['get', 'configmap', '-A', '-l', 'localctl.dev/addon=true']);
47
- return records.map((r) => {
48
- const release = r.metadata.name.replace(/-localctl-addon$/, '');
49
- return {
50
- release,
51
- owner: r.data.owner || null,
52
- namespace: r.metadata.namespace,
53
- chart: `${r.data.chart}@${r.data.version}`,
54
- ready: readyLabel(workloadsWithPrefix(r.metadata.namespace, release)),
55
- };
56
- });
57
- }
58
-
59
- module.exports = { renderClusterAddon, enableClusterAddon, disableClusterAddon, clusterAddonInstances };
1
+ const { run } = require('./exec');
2
+ const { loadAddon } = require('./addonLoader');
3
+ const { findAddonDir, listAddonNames } = require('./addonDirs');
4
+ const { getList, workloadsWithPrefix } = require('./workloadReadiness');
5
+ const { DOMAIN, OBSERVABILITY_NAMESPACE } = require('./constants');
6
+ const ui = require('./ui');
7
+
8
+ // Cluster-wide addons live at <addons-dir>/cluster/<name>/addon.yaml - your own addons dir
9
+ // (~/.localctl/addons, or $LOCALCTL_ADDONS_DIR) is searched before the ones bundled with this
10
+ // CLI, and a name there overrides a built-in one of the same name. See docs/adding-tools.md.
11
+ // Shared by `localctl addons` and `localctl profiles new`'s "carry over which addons?" wizard.
12
+ function addonDirFor(name) {
13
+ const dir = findAddonDir(name, 'cluster');
14
+ if (!dir) {
15
+ throw new Error(`Unknown addon "${name}". Known: ${listAddonNames('cluster').join(', ')}.`);
16
+ }
17
+ return dir;
18
+ }
19
+
20
+ function renderClusterAddon(name) {
21
+ const { manifestYaml } = loadAddon(addonDirFor(name), {
22
+ release: name,
23
+ namespace: OBSERVABILITY_NAMESPACE,
24
+ addonName: name,
25
+ extraVars: { domain: DOMAIN },
26
+ });
27
+ return manifestYaml;
28
+ }
29
+
30
+ function enableClusterAddon(name) {
31
+ run('kubectl', ['apply', '-f', '-'], { stdio: ['pipe', 'inherit', 'inherit'], input: renderClusterAddon(name) });
32
+ }
33
+
34
+ function disableClusterAddon(name) {
35
+ run('kubectl', ['delete', '-f', '-', '--ignore-not-found=true'], { stdio: ['pipe', 'inherit', 'inherit'], input: renderClusterAddon(name) });
36
+ }
37
+
38
+ function readyLabel(workloads) {
39
+ if (workloads.length === 0) return ui.dim('-');
40
+ const ready = workloads.reduce((sum, w) => sum + w.ready, 0);
41
+ const desired = workloads.reduce((sum, w) => sum + w.desired, 0);
42
+ return ui.readyLabel(ready, desired);
43
+ }
44
+
45
+ function clusterAddonInstances() {
46
+ const records = getList(['get', 'configmap', '-A', '-l', 'localctl.dev/addon=true']);
47
+ return records.map((r) => {
48
+ const release = r.metadata.name.replace(/-localctl-addon$/, '');
49
+ return {
50
+ release,
51
+ owner: r.data.owner || null,
52
+ namespace: r.metadata.namespace,
53
+ chart: `${r.data.chart}@${r.data.version}`,
54
+ ready: readyLabel(workloadsWithPrefix(r.metadata.namespace, release)),
55
+ };
56
+ });
57
+ }
58
+
59
+ module.exports = { renderClusterAddon, enableClusterAddon, disableClusterAddon, clusterAddonInstances };