@localctl/cli 0.5.0-beta.1 → 0.5.0-beta.3
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/README.md +9 -3
- package/package.json +4 -2
- package/src/commands/uninstall.js +3 -4
- package/src/lib/addonDirs.js +2 -1
- package/src/lib/clusterProvision.js +107 -31
- package/src/lib/exec.js +6 -3
- package/src/lib/hostsFile.js +4 -2
- package/src/lib/linuxDistro.js +30 -10
- package/src/lib/paths.js +5 -1
package/README.md
CHANGED
|
@@ -113,8 +113,9 @@ CA trust.
|
|
|
113
113
|
|
|
114
114
|
### Linux (experimental)
|
|
115
115
|
|
|
116
|
-
Linux support is **experimental**:
|
|
117
|
-
|
|
116
|
+
Linux support is **experimental**: the end-to-end suite passes on the distributions below, but
|
|
117
|
+
only in containers on arm64 (amd64 and bare-metal installs are untested), and `setup`/`doctor`
|
|
118
|
+
print a warning. It's published on the `beta` dist-tag:
|
|
118
119
|
|
|
119
120
|
```sh
|
|
120
121
|
npm install -g @localctl/cli@beta
|
|
@@ -125,10 +126,15 @@ Targeted distributions, all with systemd and a mkcert-supported trust store:
|
|
|
125
126
|
| Family | Distributions | Docker Engine install |
|
|
126
127
|
|---|---|---|
|
|
127
128
|
| Debian | Ubuntu, Debian, Linux Mint, Pop!_OS | `localctl setup --install-docker` (Docker's official script) |
|
|
128
|
-
| Red Hat | Fedora, RHEL, Rocky, AlmaLinux, CentOS Stream | `localctl setup --install-docker` (Docker's official script) |
|
|
129
|
+
| Red Hat | Fedora, RHEL, Rocky, AlmaLinux, CentOS Stream | `localctl setup --install-docker` (Docker's official script on Fedora, Docker's CentOS repository elsewhere) |
|
|
129
130
|
| Arch | Arch, Manjaro, EndeavourOS | `localctl setup --install-docker` (`pacman`) |
|
|
130
131
|
| SUSE | openSUSE Leap/Tumbleweed | `localctl setup --install-docker` (`zypper`) |
|
|
131
132
|
|
|
133
|
+
Verified end to end (`npm run test:e2e:distros`, see [Contributing](docs/contributing.md)): Ubuntu
|
|
134
|
+
22.04 and 24.04, Debian 12, Fedora 41, Rocky 9, openSUSE Leap 15.6 and Arch Linux ARM. Derivatives
|
|
135
|
+
resolve through `ID_LIKE` in `/etc/os-release` (or `/usr/lib/os-release`) but were not run
|
|
136
|
+
themselves.
|
|
137
|
+
|
|
132
138
|
Other distributions (Alpine, Void, Gentoo, NixOS, immutable/atomic desktops) may work if you
|
|
133
139
|
bring Docker (or Podman) and Node.js yourself, but aren't targeted. WSL needs the Windows hosts
|
|
134
140
|
file edited too.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localctl/cli",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.3",
|
|
4
4
|
"description": "localctl - local Kubernetes dev platform CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kubernetes",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"sync-assets": "node scripts/sync-assets.js",
|
|
37
37
|
"prepack": "node scripts/sync-assets.js",
|
|
38
38
|
"test": "node scripts/smoke-test.js",
|
|
39
|
-
"test:pack": "node scripts/pack-test.js"
|
|
39
|
+
"test:pack": "node scripts/pack-test.js",
|
|
40
|
+
"test:e2e": "node scripts/e2e/run.js",
|
|
41
|
+
"test:e2e:distros": "node scripts/e2e/distros.js"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
44
|
"chalk": "^4.1.2",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const os = require('os');
|
|
2
1
|
const fs = require('fs');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
const { isRunning } = require('../lib/exec');
|
|
@@ -8,7 +7,7 @@ const { teardownInsecureRegistry } = require('../lib/podman');
|
|
|
8
7
|
const { clearHosts } = require('../lib/hostsFile');
|
|
9
8
|
const { listProfiles } = require('../lib/profileStore');
|
|
10
9
|
const { confirm } = require('../lib/prompt');
|
|
11
|
-
const { RUN_DIR, PROFILES_PATH } = require('../lib/paths');
|
|
10
|
+
const { RUN_DIR, PROFILES_PATH, CERTS_DIR, APPS_REGISTRY_PATH } = require('../lib/paths');
|
|
12
11
|
const logger = require('../lib/logger');
|
|
13
12
|
|
|
14
13
|
// Every Main (cluster-isolation) project this machine knows about - each one owns a real,
|
|
@@ -80,8 +79,8 @@ async function uninstall(opts) {
|
|
|
80
79
|
fs.rmSync(RUN_DIR, { recursive: true, force: true });
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
const certsDir =
|
|
84
|
-
const appsRegistry =
|
|
82
|
+
const certsDir = CERTS_DIR;
|
|
83
|
+
const appsRegistry = APPS_REGISTRY_PATH;
|
|
85
84
|
if (fs.existsSync(certsDir)) fs.rmSync(certsDir, { recursive: true, force: true });
|
|
86
85
|
if (fs.existsSync(appsRegistry)) fs.rmSync(appsRegistry, { force: true });
|
|
87
86
|
if (fs.existsSync(PROFILES_PATH)) fs.rmSync(PROFILES_PATH, { force: true });
|
package/src/lib/addonDirs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { HOME_STATE_DIR } = require('./paths');
|
|
4
5
|
|
|
5
6
|
// Addons are searched in two places, in precedence order:
|
|
6
7
|
// 1. USER_DIR - yours, never touched by a `localctl` update/reinstall. Defaults to
|
|
@@ -11,7 +12,7 @@ const path = require('path');
|
|
|
11
12
|
// This is what lets someone install this tool without inheriting its author's addon set, and
|
|
12
13
|
// without forking the tool to add their own - see docs/adding-tools.md.
|
|
13
14
|
const BUILTIN_DIR = path.join(__dirname, 'addons');
|
|
14
|
-
const USER_DIR = process.env.LOCALCTL_ADDONS_DIR || path.join(
|
|
15
|
+
const USER_DIR = process.env.LOCALCTL_ADDONS_DIR || path.join(HOME_STATE_DIR, 'addons');
|
|
15
16
|
|
|
16
17
|
function roots(...segments) {
|
|
17
18
|
return [path.join(USER_DIR, ...segments), path.join(BUILTIN_DIR, ...segments)];
|
|
@@ -8,6 +8,7 @@ const { installIfMissing, installTiltWindows, nssInstallHint } = require('./tool
|
|
|
8
8
|
const { ensureInsecureRegistry, ensurePodmanSocket, blockedPrivilegedPorts } = require('./podman');
|
|
9
9
|
const { DOMAIN } = require('./constants');
|
|
10
10
|
const logger = require('./logger');
|
|
11
|
+
const { CERTS_DIR } = require('./paths');
|
|
11
12
|
const { detectDistro, dockerInstallSteps } = require('./linuxDistro');
|
|
12
13
|
|
|
13
14
|
// Cluster-level manifests: bundled under cli/assets/ in the npm package (copied there by
|
|
@@ -43,6 +44,12 @@ function clusterExists(clusterName) {
|
|
|
43
44
|
// which also repairs clusters created before this fix.
|
|
44
45
|
function useClusterContext(clusterName) {
|
|
45
46
|
const context = `k3d-${clusterName}`;
|
|
47
|
+
// The cluster can exist while this kubeconfig knows nothing about it (a deleted or replaced
|
|
48
|
+
// ~/.kube/config, a different KUBECONFIG): ask k3d to write it back instead of failing.
|
|
49
|
+
const known = capture('kubectl', ['config', 'get-contexts', '-o', 'name']).stdout.split('\n');
|
|
50
|
+
if (!known.includes(context)) {
|
|
51
|
+
run('k3d', ['kubeconfig', 'merge', clusterName, '--kubeconfig-merge-default'], { stdio: 'ignore', allowFail: true });
|
|
52
|
+
}
|
|
46
53
|
run('kubectl', ['config', 'use-context', context], { stdio: 'ignore' });
|
|
47
54
|
const server = capture('kubectl', [
|
|
48
55
|
'config', 'view', '--minify', '-o', 'jsonpath={.clusters[0].cluster.server}',
|
|
@@ -84,6 +91,80 @@ function buildK3dConfig({ clusterName, httpPort, httpsPort, registryName }) {
|
|
|
84
91
|
};
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
// Right after a (re)start the load balancer can accept connections before the API server is
|
|
95
|
+
// answering (EOF / connection refused), so wait for it before the first real kubectl call.
|
|
96
|
+
// Also after an engine restart the server container can get a new IP while the load balancer
|
|
97
|
+
// (nginx, resolves upstreams only at startup) keeps the old one, so the API never answers;
|
|
98
|
+
// restarting the load balancer once re-resolves it. Returns true once the API is ready.
|
|
99
|
+
function waitForClusterApi(engineBin, clusterName, { attempts = 60, quiet = false } = {}) {
|
|
100
|
+
if (!quiet) logger.step('Waiting for the Kubernetes API...');
|
|
101
|
+
let lbRestarted = false;
|
|
102
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
103
|
+
if (capture('kubectl', ['get', '--raw', '/readyz']).status === 0) return true;
|
|
104
|
+
if (i === 5 && !lbRestarted) {
|
|
105
|
+
lbRestarted = true;
|
|
106
|
+
run(engineBin, ['restart', `k3d-${clusterName}-serverlb`], { stdio: 'ignore', allowFail: true });
|
|
107
|
+
}
|
|
108
|
+
sleepSync(2000);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// k3d's --wait only waits for node readiness, not for Traefik's helm-install job to finish
|
|
114
|
+
// registering its CRDs, so applying TLSStore any earlier races and fails intermittently. On a slow
|
|
115
|
+
// machine (first image pulls, a nested engine) that takes minutes, so poll for the CRD to be
|
|
116
|
+
// *Established* instead of giving up after one fixed wait, and say why if it never is.
|
|
117
|
+
function waitForTraefikCrds(attempts = 150) {
|
|
118
|
+
logger.step('Waiting for Traefik CRDs...');
|
|
119
|
+
let last = 'not found yet';
|
|
120
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
121
|
+
const r = capture('kubectl', [
|
|
122
|
+
'get', 'crd', 'tlsstores.traefik.io',
|
|
123
|
+
'-o', 'jsonpath={.status.conditions[?(@.type=="Established")].status}',
|
|
124
|
+
]);
|
|
125
|
+
if (r.status === 0 && r.stdout === 'True') return;
|
|
126
|
+
last = r.status === 0 ? `Established=${r.stdout || 'unknown'}` : (r.stderr.split('\n')[0] || 'not found yet');
|
|
127
|
+
sleepSync(2000);
|
|
128
|
+
}
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Traefik's CRDs (tlsstores.traefik.io) were not ready after ${Math.round((attempts * 2) / 60)} minutes (${last}). ` +
|
|
131
|
+
'Check "kubectl -n kube-system get pods" - the traefik helm-install jobs are probably still pulling images.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The CRDs existing does not mean Traefik is serving: on a slow machine its pod can still be
|
|
136
|
+
// starting minutes later, and an app deployed in that window answers with a dropped connection.
|
|
137
|
+
// Waiting here makes "cluster ready" mean the ingress actually accepts traffic.
|
|
138
|
+
function waitForTraefik(attempts = 150) {
|
|
139
|
+
logger.step('Waiting for Traefik to accept traffic...');
|
|
140
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
141
|
+
if (capture('kubectl', ['get', 'deployment', 'traefik', '-n', 'kube-system']).status === 0) break;
|
|
142
|
+
sleepSync(2000);
|
|
143
|
+
}
|
|
144
|
+
const res = capture('kubectl', ['rollout', 'status', 'deployment/traefik', '-n', 'kube-system', '--timeout=300s']);
|
|
145
|
+
if (res.status !== 0) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Traefik did not become ready in 5 minutes (${(res.stderr || res.stdout).split('\n')[0]}). ` +
|
|
148
|
+
'Check "kubectl -n kube-system get pods".',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A CRD can be Established a moment before the API server's discovery serves it, which surfaces as
|
|
154
|
+
// "no matches for kind". Retrying the apply covers that window.
|
|
155
|
+
function applyWithRetry(file, attempts = 15) {
|
|
156
|
+
let res;
|
|
157
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
158
|
+
res = capture('kubectl', ['apply', '-f', file]);
|
|
159
|
+
if (res.status === 0) {
|
|
160
|
+
if (res.stdout) console.log(res.stdout);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
sleepSync(2000);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`kubectl apply -f ${path.basename(file)} failed after ${attempts} attempts: ${res.stderr}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
87
168
|
// Brings up (idempotently) one Main project's cluster: container engine + tool checks, the k3d
|
|
88
169
|
// cluster itself (own ports, since two clusters can't both bind host :80/:443), its own local
|
|
89
170
|
// registry container, and the shared *.local.test TLS cert wired into its Traefik. Used by both
|
|
@@ -99,13 +180,18 @@ function provisionCluster({ clusterName, httpPort, httpsPort, registryPort }) {
|
|
|
99
180
|
if (!detected) {
|
|
100
181
|
let linuxHint = '';
|
|
101
182
|
if (process.platform === 'linux') {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'
|
|
105
|
-
|
|
106
|
-
|
|
183
|
+
if (commandExists('docker') || commandExists('podman')) {
|
|
184
|
+
// Installed but not usable: reinstalling won't help, the daemon or permissions are the issue.
|
|
185
|
+
linuxHint = '\nA container engine is installed but not responding. Start the service' +
|
|
186
|
+
' (`sudo systemctl enable --now docker`, or `systemctl --user enable --now podman.socket` for Podman;' +
|
|
187
|
+
' without systemd, start `dockerd` yourself) and make sure your user can reach it' +
|
|
188
|
+
' (member of the "docker" group, then log out and in).';
|
|
107
189
|
} else {
|
|
108
|
-
|
|
190
|
+
const { family, id } = detectDistro();
|
|
191
|
+
const steps = dockerInstallSteps(family, id);
|
|
192
|
+
linuxHint = steps
|
|
193
|
+
? `\nTo install Docker Engine, run "localctl setup --install-docker", or by hand:\n ${steps.join('\n ')}`
|
|
194
|
+
: '\nInstall Docker Engine for your distro: https://docs.docker.com/engine/install/';
|
|
109
195
|
}
|
|
110
196
|
}
|
|
111
197
|
throw new Error(`No running container engine found. Start Docker Desktop, Podman, or Rancher Desktop and re-run.${linuxHint}`);
|
|
@@ -171,20 +257,15 @@ function provisionCluster({ clusterName, httpPort, httpsPort, registryPort }) {
|
|
|
171
257
|
}
|
|
172
258
|
useClusterContext(clusterName);
|
|
173
259
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (i === 5 && !lbRestarted) {
|
|
184
|
-
lbRestarted = true;
|
|
185
|
-
run(engineBin, ['restart', `k3d-${clusterName}-serverlb`], { stdio: 'ignore', allowFail: true });
|
|
186
|
-
}
|
|
187
|
-
sleepSync(2000);
|
|
260
|
+
// Up to ~5 minutes: k3s can exit once during start-up (a known race between its cloud controller
|
|
261
|
+
// and RBAC bootstrap) and is restarted by the engine, which a 2-minute wait would not outlast on
|
|
262
|
+
// a slow machine. If it never answers, stop here with the reason instead of failing later on an
|
|
263
|
+
// unrelated "EOF" from kubectl apply.
|
|
264
|
+
if (!waitForClusterApi(engineBin, clusterName, { attempts: 150 })) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`The Kubernetes API of cluster '${clusterName}' did not become ready in 5 minutes. ` +
|
|
267
|
+
`See why with: ${engineBin} logs --tail 50 k3d-${clusterName}-server-0`,
|
|
268
|
+
);
|
|
188
269
|
}
|
|
189
270
|
|
|
190
271
|
const registryNetwork = `k3d-${clusterName}`;
|
|
@@ -209,7 +290,7 @@ function provisionCluster({ clusterName, httpPort, httpsPort, registryPort }) {
|
|
|
209
290
|
// domain everywhere; only the namespace/subdomain prefix or the cluster differs).
|
|
210
291
|
logger.step(`Setting up local TLS for *.${DOMAIN}...`);
|
|
211
292
|
run('mkcert', ['-install']);
|
|
212
|
-
const certDir =
|
|
293
|
+
const certDir = CERTS_DIR;
|
|
213
294
|
fs.mkdirSync(certDir, { recursive: true });
|
|
214
295
|
const certFile = path.join(certDir, 'local-dev.pem');
|
|
215
296
|
const keyFile = path.join(certDir, 'local-dev-key.pem');
|
|
@@ -226,15 +307,10 @@ function provisionCluster({ clusterName, httpPort, httpsPort, registryPort }) {
|
|
|
226
307
|
|
|
227
308
|
// k3d's --wait only waits for node readiness, not for Traefik's helm-install job to finish
|
|
228
309
|
// registering its CRDs, so applying TLSStore any earlier races and fails intermittently.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
run('kubectl', ['wait', '--for=condition=Established', 'crd/tlsstores.traefik.io', '--timeout=60s'], { stdio: 'ignore' });
|
|
235
|
-
|
|
236
|
-
run('kubectl', ['apply', '-f', path.join(MANIFESTS_DIR, 'tls-store.yaml')]);
|
|
237
|
-
run('kubectl', ['apply', '-f', path.join(MANIFESTS_DIR, 'namespace-addons.yaml')]);
|
|
310
|
+
waitForTraefikCrds();
|
|
311
|
+
applyWithRetry(path.join(MANIFESTS_DIR, 'tls-store.yaml'));
|
|
312
|
+
applyWithRetry(path.join(MANIFESTS_DIR, 'namespace-addons.yaml'));
|
|
313
|
+
waitForTraefik();
|
|
238
314
|
logger.success('TLS configured, default cert wired into Traefik');
|
|
239
315
|
}
|
|
240
316
|
|
|
@@ -267,4 +343,4 @@ function ensureClusterUp(clusterName, profileName) {
|
|
|
267
343
|
useClusterContext(clusterName);
|
|
268
344
|
}
|
|
269
345
|
|
|
270
|
-
module.exports = { buildK3dConfig, provisionCluster, teardownCluster, clusterExists, registryNameFor, ensureClusterUp };
|
|
346
|
+
module.exports = { buildK3dConfig, useClusterContext, waitForClusterApi, provisionCluster, teardownCluster, clusterExists, registryNameFor, ensureClusterUp };
|
package/src/lib/exec.js
CHANGED
|
@@ -21,9 +21,12 @@ function capture(cmd, args, opts = {}) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function commandExists(cmd) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
return spawnSync('where', [cmd], { stdio: 'ignore' }).status === 0;
|
|
26
|
+
}
|
|
27
|
+
// `command -v` is a POSIX shell builtin; `which` is a separate program that minimal installs
|
|
28
|
+
// (Fedora/openSUSE server images, containers) don't ship.
|
|
29
|
+
return spawnSync('sh', ['-c', 'command -v "$0"', cmd], { stdio: 'ignore' }).status === 0;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
// Blocking sleep with no native dependency - used for poll loops (e.g. waiting on a CRD to
|
package/src/lib/hostsFile.js
CHANGED
|
@@ -8,8 +8,10 @@ const { DOMAIN } = require('./constants');
|
|
|
8
8
|
|
|
9
9
|
const MARK_START = '# >>> localctl managed block >>>';
|
|
10
10
|
const MARK_END = '# <<< localctl managed block <<<';
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// $LOCALCTL_HOSTS_FILE points it at another file - used by the e2e suite so tests never touch
|
|
12
|
+
// (or need sudo for) the real hosts file.
|
|
13
|
+
const HOSTS_PATH = process.env.LOCALCTL_HOSTS_FILE ||
|
|
14
|
+
(process.platform === 'win32' ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts');
|
|
13
15
|
|
|
14
16
|
function registerApp(subdomain) {
|
|
15
17
|
ensureHomeState();
|
package/src/lib/linuxDistro.js
CHANGED
|
@@ -17,12 +17,17 @@ const FAMILY_BY_ID = [
|
|
|
17
17
|
// Reads ID and ID_LIKE from os-release, so derivatives resolve to their parent (Mint, Pop!_OS ->
|
|
18
18
|
// debian; Rocky, Alma -> rhel; Manjaro -> arch).
|
|
19
19
|
function detectDistro() {
|
|
20
|
-
let release =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
let release = null;
|
|
21
|
+
// /usr/lib/os-release is the documented fallback when /etc/os-release is missing.
|
|
22
|
+
for (const file of ['/etc/os-release', '/usr/lib/os-release']) {
|
|
23
|
+
try {
|
|
24
|
+
release = fs.readFileSync(file, 'utf8');
|
|
25
|
+
break;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// try the next location
|
|
28
|
+
}
|
|
25
29
|
}
|
|
30
|
+
if (release === null) return { id: 'unknown', family: 'unknown' };
|
|
26
31
|
const value = (key) => {
|
|
27
32
|
const m = release.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
28
33
|
return m ? m[1].replace(/"/g, '').trim() : '';
|
|
@@ -43,11 +48,21 @@ function isRoot() {
|
|
|
43
48
|
|
|
44
49
|
// What `localctl setup --install-docker` would do, as printable steps. Shown in the "no engine"
|
|
45
50
|
// error too, so the user can run them by hand. Returns null for families without a known recipe.
|
|
46
|
-
function dockerInstallSteps(family) {
|
|
51
|
+
function dockerInstallSteps(family, id) {
|
|
47
52
|
const sudo = isRoot() ? '' : 'sudo ';
|
|
48
53
|
const user = os.userInfo().username;
|
|
49
54
|
let install;
|
|
50
|
-
if (family === '
|
|
55
|
+
if (family === 'rhel' && id !== 'fedora') {
|
|
56
|
+
// Docker's convenience script derives the repo path from the distro ID, and publishes
|
|
57
|
+
// .../linux/centos and .../linux/rhel but not .../linux/rocky or .../linux/almalinux: it adds
|
|
58
|
+
// a repo whose packages don't exist ("Unable to find a match: docker-ce"). The CentOS repo is
|
|
59
|
+
// what Docker documents for the RHEL rebuilds.
|
|
60
|
+
install = [
|
|
61
|
+
`${sudo}dnf -y install dnf-plugins-core`,
|
|
62
|
+
`${sudo}dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo`,
|
|
63
|
+
`${sudo}dnf -y install docker-ce docker-ce-cli containerd.io`,
|
|
64
|
+
];
|
|
65
|
+
} else if (family === 'debian' || family === 'rhel') {
|
|
51
66
|
install = [`curl -fsSL https://get.docker.com -o get-docker.sh && ${sudo}sh get-docker.sh`];
|
|
52
67
|
} else if (family === 'arch') {
|
|
53
68
|
install = [`${sudo}pacman -S --noconfirm docker`];
|
|
@@ -67,17 +82,18 @@ function dockerInstallSteps(family) {
|
|
|
67
82
|
// ever runs on an explicit `--install-docker`.
|
|
68
83
|
function installDocker() {
|
|
69
84
|
const { family, id } = detectDistro();
|
|
70
|
-
if (!dockerInstallSteps(family)) {
|
|
85
|
+
if (!dockerInstallSteps(family, id)) {
|
|
71
86
|
throw new Error(`No automatic Docker install for "${id}". Install Docker Engine for your distro (https://docs.docker.com/engine/install/) and re-run.`);
|
|
72
87
|
}
|
|
73
|
-
|
|
88
|
+
const viaScript = family === 'debian' || (family === 'rhel' && id === 'fedora');
|
|
89
|
+
if (!commandExists('curl') && viaScript) {
|
|
74
90
|
throw new Error('curl is needed to download the Docker install script. Install it and re-run.');
|
|
75
91
|
}
|
|
76
92
|
logger.step(`Installing Docker Engine (${id}). This needs sudo and will prompt for your password...`);
|
|
77
93
|
const sudo = isRoot() ? [] : ['sudo'];
|
|
78
94
|
const exec = (cmd, args) => run(sudo.length ? sudo[0] : cmd, sudo.length ? [cmd, ...args] : args);
|
|
79
95
|
|
|
80
|
-
if (
|
|
96
|
+
if (viaScript) {
|
|
81
97
|
const script = path.join(os.tmpdir(), 'localctl-get-docker.sh');
|
|
82
98
|
run('curl', ['-fsSL', 'https://get.docker.com', '-o', script]);
|
|
83
99
|
try {
|
|
@@ -85,6 +101,10 @@ function installDocker() {
|
|
|
85
101
|
} finally {
|
|
86
102
|
fs.rmSync(script, { force: true });
|
|
87
103
|
}
|
|
104
|
+
} else if (family === 'rhel') {
|
|
105
|
+
exec('dnf', ['-y', 'install', 'dnf-plugins-core']);
|
|
106
|
+
exec('dnf', ['config-manager', '--add-repo', 'https://download.docker.com/linux/centos/docker-ce.repo']);
|
|
107
|
+
exec('dnf', ['-y', 'install', 'docker-ce', 'docker-ce-cli', 'containerd.io']);
|
|
88
108
|
} else if (family === 'arch') {
|
|
89
109
|
exec('pacman', ['-S', '--noconfirm', 'docker']);
|
|
90
110
|
} else if (family === 'suse') {
|
package/src/lib/paths.js
CHANGED
|
@@ -5,7 +5,10 @@ const fs = require('fs');
|
|
|
5
5
|
const APP_DIR = process.cwd();
|
|
6
6
|
const LOCAL_DIR = path.join(APP_DIR, '.local');
|
|
7
7
|
const CONFIG_PATH = path.join(LOCAL_DIR, 'config.json');
|
|
8
|
-
|
|
8
|
+
// $LOCALCTL_HOME relocates all machine-level state (profiles, apps, certs, run files). The e2e
|
|
9
|
+
// suite uses it to run against a throwaway directory instead of your real ~/.localctl.
|
|
10
|
+
const HOME_STATE_DIR = process.env.LOCALCTL_HOME || path.join(os.homedir(), '.localctl');
|
|
11
|
+
const CERTS_DIR = path.join(HOME_STATE_DIR, 'certs');
|
|
9
12
|
const APPS_REGISTRY_PATH = path.join(HOME_STATE_DIR, 'apps.json');
|
|
10
13
|
const PROFILES_PATH = path.join(HOME_STATE_DIR, 'profiles.json');
|
|
11
14
|
const RUN_DIR = path.join(HOME_STATE_DIR, 'run');
|
|
@@ -33,6 +36,7 @@ module.exports = {
|
|
|
33
36
|
LOCAL_DIR,
|
|
34
37
|
CONFIG_PATH,
|
|
35
38
|
HOME_STATE_DIR,
|
|
39
|
+
CERTS_DIR,
|
|
36
40
|
APPS_REGISTRY_PATH,
|
|
37
41
|
PROFILES_PATH,
|
|
38
42
|
RUN_DIR,
|