@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/profiles.js
CHANGED
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { run, getFreePort } = require('../lib/exec');
|
|
4
|
-
const { provisionCluster, teardownCluster, clusterExists } = require('../lib/clusterProvision');
|
|
5
|
-
const { enableClusterAddon } = require('../lib/clusterAddons');
|
|
6
|
-
const {
|
|
7
|
-
getProfile, upsertProfile, listProfiles, getActive, setActive, mainProjectFor, childrenOf,
|
|
8
|
-
} = require('../lib/profileStore');
|
|
9
|
-
const { DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, REGISTRY_HOST_PORT } = require('../lib/constants');
|
|
10
|
-
const { makePrompter } = require('../lib/prompt');
|
|
11
|
-
const ui = require('../lib/ui');
|
|
12
|
-
const logger = require('../lib/logger');
|
|
13
|
-
|
|
14
|
-
// Tears down every OTHER Main project's cluster that's currently up - used when switching to (or
|
|
15
|
-
// creating) an `exclusive` Main project, since two clusters bound to the same host :80/:443 can't
|
|
16
|
-
// run at once anyway.
|
|
17
|
-
function teardownOtherMains(exceptMainName) {
|
|
18
|
-
Object.entries(listProfiles()).forEach(([name, def]) => {
|
|
19
|
-
if (def.isolation === 'cluster' && name !== exceptMainName && clusterExists(def.cluster)) {
|
|
20
|
-
logger.step(`Tearing down "${name}"'s cluster (exclusive project switch)...`);
|
|
21
|
-
teardownCluster(def.cluster);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function bringUpMain(mainDef) {
|
|
27
|
-
provisionCluster({
|
|
28
|
-
clusterName: mainDef.cluster,
|
|
29
|
-
httpPort: mainDef.httpPort || DEFAULT_HTTP_PORT,
|
|
30
|
-
httpsPort: mainDef.httpsPort || DEFAULT_HTTPS_PORT,
|
|
31
|
-
registryPort: mainDef.registryPort || REGISTRY_HOST_PORT,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function newProfile(name) {
|
|
36
|
-
if (getProfile(name)) {
|
|
37
|
-
logger.warn(`"${name}" is already registered - this will overwrite its definition (any cluster it already owns is left running as-is).`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const { ask, close } = makePrompter();
|
|
41
|
-
try {
|
|
42
|
-
const kindAns = await ask('Own cluster (Main project), or share an existing project\'s cluster (namespace-scoped)? [cluster/namespace]', 'cluster');
|
|
43
|
-
const namespaceScoped = kindAns.trim().toLowerCase().startsWith('n');
|
|
44
|
-
|
|
45
|
-
if (namespaceScoped) {
|
|
46
|
-
const mains = Object.entries(listProfiles()).filter(([, d]) => d.isolation === 'cluster').map(([n]) => n);
|
|
47
|
-
const parent = await ask(`Parent Main project (${mains.join(', ')})`, getActive());
|
|
48
|
-
const parentDef = getProfile(parent);
|
|
49
|
-
if (!parentDef || parentDef.isolation !== 'cluster') {
|
|
50
|
-
throw new Error(`"${parent}" isn't a registered Main project. Run "localctl profiles list".`);
|
|
51
|
-
}
|
|
52
|
-
const namespacePrefix = await ask('Namespace/subdomain prefix for this project\'s apps', name);
|
|
53
|
-
upsertProfile(name, { isolation: 'namespace', parent, namespace: namespacePrefix });
|
|
54
|
-
logger.success(`Registered "${name}", namespace-scoped under "${parent}" (apps get a "${namespacePrefix}-" prefix).`);
|
|
55
|
-
|
|
56
|
-
if (!clusterExists(parentDef.cluster)) {
|
|
57
|
-
logger.warn(`Parent project "${parent}"'s cluster isn't up yet - run "localctl profiles switch ${parent}" before deploying here.`);
|
|
58
|
-
} else {
|
|
59
|
-
run('kubectl', ['config', 'use-context', `k3d-${parentDef.cluster}`], { stdio: 'ignore' });
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
const exclusiveAns = await ask('Tear down other Main projects\' clusters whenever you switch to this one? [Y/n]', 'y');
|
|
63
|
-
const exclusive = !/^n(o)?$/i.test(exclusiveAns);
|
|
64
|
-
const def = { isolation: 'cluster', cluster: `local-dev-${name}`, exclusive };
|
|
65
|
-
if (!exclusive) {
|
|
66
|
-
// Two clusters can't both bind host :80/:443 - a non-exclusive (concurrent-with-others)
|
|
67
|
-
// Main project gets its own free ports instead, at the cost of URLs needing a :port suffix.
|
|
68
|
-
def.httpPort = await getFreePort();
|
|
69
|
-
def.httpsPort = await getFreePort();
|
|
70
|
-
def.registryPort = await getFreePort();
|
|
71
|
-
logger.info(`Assigned its own ports (won't collide with an exclusive project's :80/:443): http ${def.httpPort}, https ${def.httpsPort}, registry ${def.registryPort}.`);
|
|
72
|
-
}
|
|
73
|
-
upsertProfile(name, def);
|
|
74
|
-
logger.success(`Registered Main project "${name}" (cluster "${def.cluster}").`);
|
|
75
|
-
|
|
76
|
-
if (exclusive) teardownOtherMains(name);
|
|
77
|
-
logger.step(`Bringing up "${name}"'s cluster...`);
|
|
78
|
-
bringUpMain(def);
|
|
79
|
-
|
|
80
|
-
const addonAns = await ask('Cluster-wide addons to enable here now, comma-separated (blank for none)', '');
|
|
81
|
-
addonAns.split(',').map((s) => s.trim()).filter(Boolean).forEach((addonName) => {
|
|
82
|
-
logger.step(`Enabling addon "${addonName}"...`);
|
|
83
|
-
enableClusterAddon(addonName);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
setActive(name);
|
|
88
|
-
|
|
89
|
-
const localDir = path.join(process.cwd(), '.local');
|
|
90
|
-
fs.mkdirSync(localDir, { recursive: true });
|
|
91
|
-
fs.writeFileSync(path.join(localDir, 'project.json'), `${JSON.stringify({ profile: name }, null, 2)}\n`);
|
|
92
|
-
logger.success(`Wrote .local/project.json here - every app under ${process.cwd()} now belongs to "${name}" unless a subfolder overrides it.`);
|
|
93
|
-
} finally {
|
|
94
|
-
close();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function switchProfile(name) {
|
|
99
|
-
const target = getProfile(name);
|
|
100
|
-
if (!target) {
|
|
101
|
-
throw new Error(`Unknown profile "${name}". Run "localctl profiles list", or "localctl profiles new ${name}" to create it.`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const { name: mainName, def: mainDef } = mainProjectFor(name);
|
|
105
|
-
if (mainDef.exclusive) teardownOtherMains(mainName);
|
|
106
|
-
|
|
107
|
-
if (!clusterExists(mainDef.cluster)) {
|
|
108
|
-
logger.step(`Bringing up "${mainName}"'s cluster...`);
|
|
109
|
-
bringUpMain(mainDef);
|
|
110
|
-
} else {
|
|
111
|
-
run('kubectl', ['config', 'use-context', `k3d-${mainDef.cluster}`], { stdio: 'ignore' });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
setActive(name);
|
|
115
|
-
logger.success(`Switched to "${name}"${name !== mainName ? ` (namespace-scoped under "${mainName}")` : ''}.`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function listProfilesCmd() {
|
|
119
|
-
const profiles = listProfiles();
|
|
120
|
-
const active = getActive();
|
|
121
|
-
|
|
122
|
-
Object.entries(profiles)
|
|
123
|
-
.filter(([, def]) => def.isolation === 'cluster')
|
|
124
|
-
.forEach(([name, def]) => {
|
|
125
|
-
const up = clusterExists(def.cluster);
|
|
126
|
-
const marker = name === active ? ui.ok('*') : ' ';
|
|
127
|
-
const portsNote = def.exclusive ? '' : ` http:${def.httpPort} https:${def.httpsPort}`;
|
|
128
|
-
console.log(
|
|
129
|
-
`${marker} ${ui.bold(name)} [cluster: ${def.cluster}, ${up ? ui.ok('up') : ui.dim('down')}, ` +
|
|
130
|
-
`${def.exclusive ? 'exclusive' : 'concurrent'}${portsNote}]`,
|
|
131
|
-
);
|
|
132
|
-
childrenOf(name).forEach((child) => {
|
|
133
|
-
const childMarker = child === active ? ui.ok('*') : ' ';
|
|
134
|
-
const childDef = profiles[child];
|
|
135
|
-
console.log(` ${childMarker} ${child} [namespace: ${childDef.namespace || child}-*]`);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function statusCmd() {
|
|
141
|
-
const active = getActive();
|
|
142
|
-
const def = getProfile(active);
|
|
143
|
-
const { name: mainName, def: mainDef } = mainProjectFor(active);
|
|
144
|
-
|
|
145
|
-
console.log(ui.bold(active));
|
|
146
|
-
console.log(` isolation: ${def.isolation}`);
|
|
147
|
-
if (def.isolation === 'namespace') {
|
|
148
|
-
console.log(` parent: ${def.parent}`);
|
|
149
|
-
console.log(` namespace: ${def.namespace || active}-*`);
|
|
150
|
-
}
|
|
151
|
-
console.log(` cluster: ${mainDef.cluster} (${clusterExists(mainDef.cluster) ? ui.ok('up') : ui.dim('down')})`);
|
|
152
|
-
if (!mainDef.exclusive) {
|
|
153
|
-
console.log(` ports: http ${mainDef.httpPort} / https ${mainDef.httpsPort} / registry ${mainDef.registryPort}`);
|
|
154
|
-
}
|
|
155
|
-
console.log(` exclusive: ${mainDef.exclusive ? 'yes - switching away tears this cluster down' : 'no - can run alongside other projects'}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
module.exports = (program) => {
|
|
159
|
-
const profiles = program.command('profiles').description('Manage projects: isolated clusters or namespace-scoped slices of one');
|
|
160
|
-
|
|
161
|
-
profiles
|
|
162
|
-
.command('new <name>')
|
|
163
|
-
.description('Create a project - its own cluster, or namespace-scoped under an existing one - and mark the current directory as belonging to it')
|
|
164
|
-
.action(newProfile);
|
|
165
|
-
|
|
166
|
-
profiles
|
|
167
|
-
.command('list')
|
|
168
|
-
.description('List every project this machine knows about, nested under its Main (cluster) project')
|
|
169
|
-
.action(listProfilesCmd);
|
|
170
|
-
|
|
171
|
-
profiles
|
|
172
|
-
.command('switch <name>')
|
|
173
|
-
.description('Switch the active project - brings its cluster up if needed, applies its exclusive-teardown setting')
|
|
174
|
-
.action(switchProfile);
|
|
175
|
-
|
|
176
|
-
profiles
|
|
177
|
-
.command('status')
|
|
178
|
-
.description('Show the currently active project')
|
|
179
|
-
.action(statusCmd);
|
|
180
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { run, getFreePort } = require('../lib/exec');
|
|
4
|
+
const { provisionCluster, teardownCluster, clusterExists } = require('../lib/clusterProvision');
|
|
5
|
+
const { enableClusterAddon } = require('../lib/clusterAddons');
|
|
6
|
+
const {
|
|
7
|
+
getProfile, upsertProfile, listProfiles, getActive, setActive, mainProjectFor, childrenOf,
|
|
8
|
+
} = require('../lib/profileStore');
|
|
9
|
+
const { DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, REGISTRY_HOST_PORT } = require('../lib/constants');
|
|
10
|
+
const { makePrompter } = require('../lib/prompt');
|
|
11
|
+
const ui = require('../lib/ui');
|
|
12
|
+
const logger = require('../lib/logger');
|
|
13
|
+
|
|
14
|
+
// Tears down every OTHER Main project's cluster that's currently up - used when switching to (or
|
|
15
|
+
// creating) an `exclusive` Main project, since two clusters bound to the same host :80/:443 can't
|
|
16
|
+
// run at once anyway.
|
|
17
|
+
function teardownOtherMains(exceptMainName) {
|
|
18
|
+
Object.entries(listProfiles()).forEach(([name, def]) => {
|
|
19
|
+
if (def.isolation === 'cluster' && name !== exceptMainName && clusterExists(def.cluster)) {
|
|
20
|
+
logger.step(`Tearing down "${name}"'s cluster (exclusive project switch)...`);
|
|
21
|
+
teardownCluster(def.cluster);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function bringUpMain(mainDef) {
|
|
27
|
+
provisionCluster({
|
|
28
|
+
clusterName: mainDef.cluster,
|
|
29
|
+
httpPort: mainDef.httpPort || DEFAULT_HTTP_PORT,
|
|
30
|
+
httpsPort: mainDef.httpsPort || DEFAULT_HTTPS_PORT,
|
|
31
|
+
registryPort: mainDef.registryPort || REGISTRY_HOST_PORT,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function newProfile(name) {
|
|
36
|
+
if (getProfile(name)) {
|
|
37
|
+
logger.warn(`"${name}" is already registered - this will overwrite its definition (any cluster it already owns is left running as-is).`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { ask, close } = makePrompter();
|
|
41
|
+
try {
|
|
42
|
+
const kindAns = await ask('Own cluster (Main project), or share an existing project\'s cluster (namespace-scoped)? [cluster/namespace]', 'cluster');
|
|
43
|
+
const namespaceScoped = kindAns.trim().toLowerCase().startsWith('n');
|
|
44
|
+
|
|
45
|
+
if (namespaceScoped) {
|
|
46
|
+
const mains = Object.entries(listProfiles()).filter(([, d]) => d.isolation === 'cluster').map(([n]) => n);
|
|
47
|
+
const parent = await ask(`Parent Main project (${mains.join(', ')})`, getActive());
|
|
48
|
+
const parentDef = getProfile(parent);
|
|
49
|
+
if (!parentDef || parentDef.isolation !== 'cluster') {
|
|
50
|
+
throw new Error(`"${parent}" isn't a registered Main project. Run "localctl profiles list".`);
|
|
51
|
+
}
|
|
52
|
+
const namespacePrefix = await ask('Namespace/subdomain prefix for this project\'s apps', name);
|
|
53
|
+
upsertProfile(name, { isolation: 'namespace', parent, namespace: namespacePrefix });
|
|
54
|
+
logger.success(`Registered "${name}", namespace-scoped under "${parent}" (apps get a "${namespacePrefix}-" prefix).`);
|
|
55
|
+
|
|
56
|
+
if (!clusterExists(parentDef.cluster)) {
|
|
57
|
+
logger.warn(`Parent project "${parent}"'s cluster isn't up yet - run "localctl profiles switch ${parent}" before deploying here.`);
|
|
58
|
+
} else {
|
|
59
|
+
run('kubectl', ['config', 'use-context', `k3d-${parentDef.cluster}`], { stdio: 'ignore' });
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const exclusiveAns = await ask('Tear down other Main projects\' clusters whenever you switch to this one? [Y/n]', 'y');
|
|
63
|
+
const exclusive = !/^n(o)?$/i.test(exclusiveAns);
|
|
64
|
+
const def = { isolation: 'cluster', cluster: `local-dev-${name}`, exclusive };
|
|
65
|
+
if (!exclusive) {
|
|
66
|
+
// Two clusters can't both bind host :80/:443 - a non-exclusive (concurrent-with-others)
|
|
67
|
+
// Main project gets its own free ports instead, at the cost of URLs needing a :port suffix.
|
|
68
|
+
def.httpPort = await getFreePort();
|
|
69
|
+
def.httpsPort = await getFreePort();
|
|
70
|
+
def.registryPort = await getFreePort();
|
|
71
|
+
logger.info(`Assigned its own ports (won't collide with an exclusive project's :80/:443): http ${def.httpPort}, https ${def.httpsPort}, registry ${def.registryPort}.`);
|
|
72
|
+
}
|
|
73
|
+
upsertProfile(name, def);
|
|
74
|
+
logger.success(`Registered Main project "${name}" (cluster "${def.cluster}").`);
|
|
75
|
+
|
|
76
|
+
if (exclusive) teardownOtherMains(name);
|
|
77
|
+
logger.step(`Bringing up "${name}"'s cluster...`);
|
|
78
|
+
bringUpMain(def);
|
|
79
|
+
|
|
80
|
+
const addonAns = await ask('Cluster-wide addons to enable here now, comma-separated (blank for none)', '');
|
|
81
|
+
addonAns.split(',').map((s) => s.trim()).filter(Boolean).forEach((addonName) => {
|
|
82
|
+
logger.step(`Enabling addon "${addonName}"...`);
|
|
83
|
+
enableClusterAddon(addonName);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setActive(name);
|
|
88
|
+
|
|
89
|
+
const localDir = path.join(process.cwd(), '.local');
|
|
90
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
91
|
+
fs.writeFileSync(path.join(localDir, 'project.json'), `${JSON.stringify({ profile: name }, null, 2)}\n`);
|
|
92
|
+
logger.success(`Wrote .local/project.json here - every app under ${process.cwd()} now belongs to "${name}" unless a subfolder overrides it.`);
|
|
93
|
+
} finally {
|
|
94
|
+
close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function switchProfile(name) {
|
|
99
|
+
const target = getProfile(name);
|
|
100
|
+
if (!target) {
|
|
101
|
+
throw new Error(`Unknown profile "${name}". Run "localctl profiles list", or "localctl profiles new ${name}" to create it.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { name: mainName, def: mainDef } = mainProjectFor(name);
|
|
105
|
+
if (mainDef.exclusive) teardownOtherMains(mainName);
|
|
106
|
+
|
|
107
|
+
if (!clusterExists(mainDef.cluster)) {
|
|
108
|
+
logger.step(`Bringing up "${mainName}"'s cluster...`);
|
|
109
|
+
bringUpMain(mainDef);
|
|
110
|
+
} else {
|
|
111
|
+
run('kubectl', ['config', 'use-context', `k3d-${mainDef.cluster}`], { stdio: 'ignore' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setActive(name);
|
|
115
|
+
logger.success(`Switched to "${name}"${name !== mainName ? ` (namespace-scoped under "${mainName}")` : ''}.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function listProfilesCmd() {
|
|
119
|
+
const profiles = listProfiles();
|
|
120
|
+
const active = getActive();
|
|
121
|
+
|
|
122
|
+
Object.entries(profiles)
|
|
123
|
+
.filter(([, def]) => def.isolation === 'cluster')
|
|
124
|
+
.forEach(([name, def]) => {
|
|
125
|
+
const up = clusterExists(def.cluster);
|
|
126
|
+
const marker = name === active ? ui.ok('*') : ' ';
|
|
127
|
+
const portsNote = def.exclusive ? '' : ` http:${def.httpPort} https:${def.httpsPort}`;
|
|
128
|
+
console.log(
|
|
129
|
+
`${marker} ${ui.bold(name)} [cluster: ${def.cluster}, ${up ? ui.ok('up') : ui.dim('down')}, ` +
|
|
130
|
+
`${def.exclusive ? 'exclusive' : 'concurrent'}${portsNote}]`,
|
|
131
|
+
);
|
|
132
|
+
childrenOf(name).forEach((child) => {
|
|
133
|
+
const childMarker = child === active ? ui.ok('*') : ' ';
|
|
134
|
+
const childDef = profiles[child];
|
|
135
|
+
console.log(` ${childMarker} ${child} [namespace: ${childDef.namespace || child}-*]`);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function statusCmd() {
|
|
141
|
+
const active = getActive();
|
|
142
|
+
const def = getProfile(active);
|
|
143
|
+
const { name: mainName, def: mainDef } = mainProjectFor(active);
|
|
144
|
+
|
|
145
|
+
console.log(ui.bold(active));
|
|
146
|
+
console.log(` isolation: ${def.isolation}`);
|
|
147
|
+
if (def.isolation === 'namespace') {
|
|
148
|
+
console.log(` parent: ${def.parent}`);
|
|
149
|
+
console.log(` namespace: ${def.namespace || active}-*`);
|
|
150
|
+
}
|
|
151
|
+
console.log(` cluster: ${mainDef.cluster} (${clusterExists(mainDef.cluster) ? ui.ok('up') : ui.dim('down')})`);
|
|
152
|
+
if (!mainDef.exclusive) {
|
|
153
|
+
console.log(` ports: http ${mainDef.httpPort} / https ${mainDef.httpsPort} / registry ${mainDef.registryPort}`);
|
|
154
|
+
}
|
|
155
|
+
console.log(` exclusive: ${mainDef.exclusive ? 'yes - switching away tears this cluster down' : 'no - can run alongside other projects'}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = (program) => {
|
|
159
|
+
const profiles = program.command('profiles').description('Manage projects: isolated clusters or namespace-scoped slices of one');
|
|
160
|
+
|
|
161
|
+
profiles
|
|
162
|
+
.command('new <name>')
|
|
163
|
+
.description('Create a project - its own cluster, or namespace-scoped under an existing one - and mark the current directory as belonging to it')
|
|
164
|
+
.action(newProfile);
|
|
165
|
+
|
|
166
|
+
profiles
|
|
167
|
+
.command('list')
|
|
168
|
+
.description('List every project this machine knows about, nested under its Main (cluster) project')
|
|
169
|
+
.action(listProfilesCmd);
|
|
170
|
+
|
|
171
|
+
profiles
|
|
172
|
+
.command('switch <name>')
|
|
173
|
+
.description('Switch the active project - brings its cluster up if needed, applies its exclusive-teardown setting')
|
|
174
|
+
.action(switchProfile);
|
|
175
|
+
|
|
176
|
+
profiles
|
|
177
|
+
.command('status')
|
|
178
|
+
.description('Show the currently active project')
|
|
179
|
+
.action(statusCmd);
|
|
180
|
+
};
|
package/src/commands/prune.js
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
const { loadConfig } = require('../lib/loadConfig');
|
|
2
|
-
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
3
|
-
const { run, capture } = require('../lib/exec');
|
|
4
|
-
const { getList } = require('../lib/workloadReadiness');
|
|
5
|
-
const { confirm } = require('../lib/prompt');
|
|
6
|
-
const logger = require('../lib/logger');
|
|
7
|
-
|
|
8
|
-
module.exports = (program) => {
|
|
9
|
-
program
|
|
10
|
-
.command('prune')
|
|
11
|
-
.option('-y, --yes', 'skip the confirmation prompt')
|
|
12
|
-
.description('Permanently delete an already-torn-down app\'s leftover data (dependency PVCs, secrets) - "app down" deliberately leaves these alone')
|
|
13
|
-
.action(async (opts) => {
|
|
14
|
-
const cfg = loadConfig();
|
|
15
|
-
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
16
|
-
|
|
17
|
-
if (capture('kubectl', ['get', 'deployment', cfg.name, '-n', cfg.namespace]).status === 0) {
|
|
18
|
-
throw new Error(`"${cfg.name}" is still deployed - run "localctl app down" first, then "localctl app prune" to also wipe its data.`);
|
|
19
|
-
}
|
|
20
|
-
if (capture('kubectl', ['get', 'namespace', cfg.namespace]).status !== 0) {
|
|
21
|
-
logger.info(`Nothing to prune - namespace "${cfg.namespace}" doesn't exist.`);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const pvcs = getList(['get', 'pvc', '-n', cfg.namespace]);
|
|
26
|
-
const secrets = getList(['get', 'secret', '-n', cfg.namespace]).filter((s) => s.type !== 'kubernetes.io/service-account-token');
|
|
27
|
-
if (pvcs.length === 0 && secrets.length === 0) {
|
|
28
|
-
logger.info(`Nothing left to prune in "${cfg.namespace}".`);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
console.log(`This will permanently delete everything left in namespace "${cfg.namespace}":`);
|
|
33
|
-
pvcs.forEach((p) => console.log(` - PersistentVolumeClaim/${p.metadata.name} (data loss)`));
|
|
34
|
-
secrets.forEach((s) => console.log(` - Secret/${s.metadata.name}`));
|
|
35
|
-
console.log('');
|
|
36
|
-
|
|
37
|
-
if (!opts.yes) {
|
|
38
|
-
const answer = await confirm('Continue? [y/N] ');
|
|
39
|
-
if (!/^y(es)?$/i.test(answer)) {
|
|
40
|
-
console.log('Aborted.');
|
|
41
|
-
process.exitCode = 1;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
run('kubectl', ['delete', 'namespace', cfg.namespace]);
|
|
47
|
-
logger.success(`Namespace "${cfg.namespace}" and everything in it deleted.`);
|
|
48
|
-
});
|
|
49
|
-
};
|
|
1
|
+
const { loadConfig } = require('../lib/loadConfig');
|
|
2
|
+
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
3
|
+
const { run, capture } = require('../lib/exec');
|
|
4
|
+
const { getList } = require('../lib/workloadReadiness');
|
|
5
|
+
const { confirm } = require('../lib/prompt');
|
|
6
|
+
const logger = require('../lib/logger');
|
|
7
|
+
|
|
8
|
+
module.exports = (program) => {
|
|
9
|
+
program
|
|
10
|
+
.command('prune')
|
|
11
|
+
.option('-y, --yes', 'skip the confirmation prompt')
|
|
12
|
+
.description('Permanently delete an already-torn-down app\'s leftover data (dependency PVCs, secrets) - "app down" deliberately leaves these alone')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const cfg = loadConfig();
|
|
15
|
+
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
16
|
+
|
|
17
|
+
if (capture('kubectl', ['get', 'deployment', cfg.name, '-n', cfg.namespace]).status === 0) {
|
|
18
|
+
throw new Error(`"${cfg.name}" is still deployed - run "localctl app down" first, then "localctl app prune" to also wipe its data.`);
|
|
19
|
+
}
|
|
20
|
+
if (capture('kubectl', ['get', 'namespace', cfg.namespace]).status !== 0) {
|
|
21
|
+
logger.info(`Nothing to prune - namespace "${cfg.namespace}" doesn't exist.`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pvcs = getList(['get', 'pvc', '-n', cfg.namespace]);
|
|
26
|
+
const secrets = getList(['get', 'secret', '-n', cfg.namespace]).filter((s) => s.type !== 'kubernetes.io/service-account-token');
|
|
27
|
+
if (pvcs.length === 0 && secrets.length === 0) {
|
|
28
|
+
logger.info(`Nothing left to prune in "${cfg.namespace}".`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`This will permanently delete everything left in namespace "${cfg.namespace}":`);
|
|
33
|
+
pvcs.forEach((p) => console.log(` - PersistentVolumeClaim/${p.metadata.name} (data loss)`));
|
|
34
|
+
secrets.forEach((s) => console.log(` - Secret/${s.metadata.name}`));
|
|
35
|
+
console.log('');
|
|
36
|
+
|
|
37
|
+
if (!opts.yes) {
|
|
38
|
+
const answer = await confirm('Continue? [y/N] ');
|
|
39
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
40
|
+
console.log('Aborted.');
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
run('kubectl', ['delete', 'namespace', cfg.namespace]);
|
|
47
|
+
logger.success(`Namespace "${cfg.namespace}" and everything in it deleted.`);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
const { loadConfig } = require('../lib/loadConfig');
|
|
2
|
-
const { portsFor, mainProjectFor } = require('../lib/profileStore');
|
|
3
|
-
const { registryNameFor } = require('../lib/clusterProvision');
|
|
4
|
-
|
|
5
|
-
module.exports = (program) => {
|
|
6
|
-
program
|
|
7
|
-
.command('registry-info')
|
|
8
|
-
.description('[internal, called by Tiltfile] Print this app\'s registry push/pull addresses as JSON')
|
|
9
|
-
.action(() => {
|
|
10
|
-
const cfg = loadConfig();
|
|
11
|
-
const { registryPort } = portsFor(cfg.profile);
|
|
12
|
-
const { def } = mainProjectFor(cfg.profile);
|
|
13
|
-
process.stdout.write(JSON.stringify({
|
|
14
|
-
pushHost: `localhost:${registryPort}`,
|
|
15
|
-
clusterHost: `${registryNameFor(def.cluster)}:5000`,
|
|
16
|
-
}));
|
|
17
|
-
});
|
|
18
|
-
};
|
|
1
|
+
const { loadConfig } = require('../lib/loadConfig');
|
|
2
|
+
const { portsFor, mainProjectFor } = require('../lib/profileStore');
|
|
3
|
+
const { registryNameFor } = require('../lib/clusterProvision');
|
|
4
|
+
|
|
5
|
+
module.exports = (program) => {
|
|
6
|
+
program
|
|
7
|
+
.command('registry-info')
|
|
8
|
+
.description('[internal, called by Tiltfile] Print this app\'s registry push/pull addresses as JSON')
|
|
9
|
+
.action(() => {
|
|
10
|
+
const cfg = loadConfig();
|
|
11
|
+
const { registryPort } = portsFor(cfg.profile);
|
|
12
|
+
const { def } = mainProjectFor(cfg.profile);
|
|
13
|
+
process.stdout.write(JSON.stringify({
|
|
14
|
+
pushHost: `localhost:${registryPort}`,
|
|
15
|
+
clusterHost: `${registryNameFor(def.cluster)}:5000`,
|
|
16
|
+
}));
|
|
17
|
+
});
|
|
18
|
+
};
|
package/src/commands/reload.js
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { CONFIG_PATH, runFiles } = require('../lib/paths');
|
|
3
|
-
const { run, isRunning } = require('../lib/exec');
|
|
4
|
-
const logger = require('../lib/logger');
|
|
5
|
-
|
|
6
|
-
function resolveAppName(opts) {
|
|
7
|
-
if (opts.app) return opts.app;
|
|
8
|
-
if (fs.existsSync(CONFIG_PATH)) {
|
|
9
|
-
try {
|
|
10
|
-
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')).name;
|
|
11
|
-
} catch (e) {
|
|
12
|
-
// fall through
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
module.exports = (program) => {
|
|
19
|
-
program
|
|
20
|
-
.command('reload')
|
|
21
|
-
.option('-a, --app <name>', 'reload a specific app by name, regardless of the current directory')
|
|
22
|
-
.description('Force an immediate rebuild/redeploy of a running background dev loop, without waiting on a file change')
|
|
23
|
-
.action((opts) => {
|
|
24
|
-
const name = resolveAppName(opts);
|
|
25
|
-
if (!name) {
|
|
26
|
-
throw new Error('Run this from inside an app repo, or pass -a <name>.');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const { pidFile, portFile } = runFiles(name);
|
|
30
|
-
if (!fs.existsSync(pidFile) || !fs.existsSync(portFile)) {
|
|
31
|
-
throw new Error(`No background dev loop tracked for "${name}". Run "localctl app up" first (reload only works in detached mode, not "-f/--foreground").`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
|
|
35
|
-
if (!Number.isFinite(pid) || !isRunning(pid)) {
|
|
36
|
-
throw new Error(`"${name}"'s dev loop isn't running. Run "localctl app up" to start it.`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const port = fs.readFileSync(portFile, 'utf8').trim();
|
|
40
|
-
logger.step(`Triggering an immediate rebuild/redeploy for "${name}"...`);
|
|
41
|
-
run('tilt', ['trigger', '--port', port, name]);
|
|
42
|
-
logger.success(`"${name}" reload triggered - check "localctl app logs ${name} -f" for progress.`);
|
|
43
|
-
});
|
|
44
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { CONFIG_PATH, runFiles } = require('../lib/paths');
|
|
3
|
+
const { run, isRunning } = require('../lib/exec');
|
|
4
|
+
const logger = require('../lib/logger');
|
|
5
|
+
|
|
6
|
+
function resolveAppName(opts) {
|
|
7
|
+
if (opts.app) return opts.app;
|
|
8
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')).name;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
// fall through
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = (program) => {
|
|
19
|
+
program
|
|
20
|
+
.command('reload')
|
|
21
|
+
.option('-a, --app <name>', 'reload a specific app by name, regardless of the current directory')
|
|
22
|
+
.description('Force an immediate rebuild/redeploy of a running background dev loop, without waiting on a file change')
|
|
23
|
+
.action((opts) => {
|
|
24
|
+
const name = resolveAppName(opts);
|
|
25
|
+
if (!name) {
|
|
26
|
+
throw new Error('Run this from inside an app repo, or pass -a <name>.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { pidFile, portFile } = runFiles(name);
|
|
30
|
+
if (!fs.existsSync(pidFile) || !fs.existsSync(portFile)) {
|
|
31
|
+
throw new Error(`No background dev loop tracked for "${name}". Run "localctl app up" first (reload only works in detached mode, not "-f/--foreground").`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'), 10);
|
|
35
|
+
if (!Number.isFinite(pid) || !isRunning(pid)) {
|
|
36
|
+
throw new Error(`"${name}"'s dev loop isn't running. Run "localctl app up" to start it.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const port = fs.readFileSync(portFile, 'utf8').trim();
|
|
40
|
+
logger.step(`Triggering an immediate rebuild/redeploy for "${name}"...`);
|
|
41
|
+
run('tilt', ['trigger', '--port', port, name]);
|
|
42
|
+
logger.success(`"${name}" reload triggered - check "localctl app logs ${name} -f" for progress.`);
|
|
43
|
+
});
|
|
44
|
+
};
|