@localctl/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +398 -431
- package/assets/manifests/namespace-addons.yaml +4 -4
- package/assets/manifests/tls-store.yaml +8 -8
- package/bin/localctl.js +2 -2
- package/package.json +61 -54
- package/src/commands/addons.js +56 -56
- package/src/commands/doctor.js +44 -43
- package/src/commands/down.js +35 -35
- package/src/commands/exec.js +15 -15
- package/src/commands/generateManifests.js +12 -12
- package/src/commands/hosts.js +23 -23
- package/src/commands/logs.js +45 -45
- package/src/commands/profiles.js +180 -180
- package/src/commands/prune.js +49 -49
- package/src/commands/registryInfo.js +18 -18
- package/src/commands/reload.js +44 -44
- package/src/commands/secrets.js +75 -75
- package/src/commands/setup.js +36 -36
- package/src/commands/status.js +136 -136
- package/src/commands/up.js +123 -123
- package/src/index.js +38 -36
- package/src/lib/addonDirs.js +43 -43
- package/src/lib/addonLoader.js +190 -190
- package/src/lib/addons/cluster/logging/addon.yaml +46 -46
- package/src/lib/addons/cluster/logging/config/all-logs.json +21 -21
- package/src/lib/addons/cluster/monitoring/addon.yaml +33 -33
- package/src/lib/addons/mongo/addon.yaml +18 -18
- package/src/lib/addons/postgres/addon.yaml +39 -39
- package/src/lib/addons/redis/addon.yaml +29 -29
- package/src/lib/appSecrets.js +60 -60
- package/src/lib/clusterAddons.js +59 -59
- package/src/lib/clusterProvision.js +171 -170
- package/src/lib/configSchema.js +103 -103
- package/src/lib/constants.js +16 -16
- package/src/lib/engine.js +36 -20
- package/src/lib/exec.js +77 -77
- package/src/lib/hostsFile.js +79 -79
- package/src/lib/loadConfig.js +51 -51
- package/src/lib/logger.js +9 -9
- package/src/lib/manifestGen.js +210 -210
- package/src/lib/paths.js +41 -41
- package/src/lib/podman.js +72 -72
- package/src/lib/profileStore.js +134 -134
- package/src/lib/projectContext.js +32 -32
- package/src/lib/prompt.js +97 -97
- package/src/lib/toolInstall.js +30 -30
- package/src/lib/ui.js +50 -50
- package/src/lib/updateCheck.js +84 -0
- package/src/lib/updateCheckWorker.js +46 -0
- package/src/lib/workloadReadiness.js +35 -35
- package/templates/Tiltfile.tmpl +35 -35
package/src/lib/addonLoader.js
CHANGED
|
@@ -1,190 +1,190 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
const yaml = require('js-yaml');
|
|
6
|
-
const { run, capture } = require('./exec');
|
|
7
|
-
|
|
8
|
-
// Generic Helm-based addon loader. An addon is a folder containing an addon.yaml (what chart to
|
|
9
|
-
// pull and how to configure it) and, optionally, a config/ folder (literal files - dashboards,
|
|
10
|
-
// datasources, init scripts - that don't belong in Helm values but need to reach the chart as a
|
|
11
|
-
// ConfigMap). See docs/adding-tools.md for the full addon.yaml reference and a worked example.
|
|
12
|
-
|
|
13
|
-
function repoNameFor(url) {
|
|
14
|
-
return `localctl-${crypto.createHash('md5').update(url).digest('hex').slice(0, 8)}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function ensureRepo(url) {
|
|
18
|
-
const name = repoNameFor(url);
|
|
19
|
-
// --force-update: idempotent even if re-pointed at a different URL under the same generated name.
|
|
20
|
-
run('helm', ['repo', 'add', name, url, '--force-update'], { stdio: 'ignore' });
|
|
21
|
-
run('helm', ['repo', 'update', name], { stdio: 'ignore', allowFail: true });
|
|
22
|
-
return name;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function substitute(template, vars) {
|
|
26
|
-
return String(template).replace(/\{\{\s*([\w.]+)\s*\}\}/g, (match, key) => {
|
|
27
|
-
if (!(key in vars)) throw new Error(`Addon references unknown template variable "{{${key}}}"`);
|
|
28
|
-
return vars[key];
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function ensureNamespace(ns) {
|
|
33
|
-
if (capture('kubectl', ['get', 'namespace', ns]).status !== 0) {
|
|
34
|
-
run('kubectl', ['create', 'namespace', ns], { stdio: 'ignore', allowFail: true });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Generates each secret's value once and stores it as a real k8s Secret; a re-run that finds the
|
|
39
|
-
// Secret already there leaves it alone, so app data doesn't lose its password out from under it
|
|
40
|
-
// on every `localctl up`.
|
|
41
|
-
function ensureSecret(name, namespace, keys) {
|
|
42
|
-
ensureNamespace(namespace);
|
|
43
|
-
if (capture('kubectl', ['get', 'secret', name, '-n', namespace]).status === 0) return;
|
|
44
|
-
const args = ['create', 'secret', 'generic', name, '-n', namespace];
|
|
45
|
-
for (const key of keys) {
|
|
46
|
-
args.push(`--from-literal=${key}=${crypto.randomBytes(16).toString('hex')}`);
|
|
47
|
-
}
|
|
48
|
-
run('kubectl', args, { stdio: 'ignore' });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Turns a folder of literal files (dashboards, datasource definitions, init scripts - whatever
|
|
52
|
-
// the chart expects to mount, not a Helm value) into a real ConfigMap.
|
|
53
|
-
function ensureConfigMap(name, namespace, configDir) {
|
|
54
|
-
ensureNamespace(namespace);
|
|
55
|
-
const files = fs.readdirSync(configDir).filter((f) => fs.statSync(path.join(configDir, f)).isFile());
|
|
56
|
-
const args = ['create', 'configmap', name, '-n', namespace, '--dry-run=client', '-o', 'yaml'];
|
|
57
|
-
files.forEach((f) => args.push(`--from-file=${path.join(configDir, f)}`));
|
|
58
|
-
const rendered = capture('kubectl', args);
|
|
59
|
-
if (rendered.status !== 0) throw new Error(`Failed to render ConfigMap for ${configDir}: ${rendered.stderr}`);
|
|
60
|
-
run('kubectl', ['apply', '-f', '-'], { stdio: ['pipe', 'ignore', 'inherit'], input: rendered.stdout });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function helmTemplate(release, chart, valuesObj, namespace) {
|
|
64
|
-
const repoName = ensureRepo(chart.repo);
|
|
65
|
-
const tmpValues = path.join(os.tmpdir(), `localctl-addon-values-${process.pid}-${Date.now()}.yaml`);
|
|
66
|
-
fs.writeFileSync(tmpValues, yaml.dump(valuesObj));
|
|
67
|
-
try {
|
|
68
|
-
// --no-hooks: excludes `helm test` pods and similar hook resources from the rendered output -
|
|
69
|
-
// we apply this YAML with plain `kubectl apply`, not `helm install`, so nothing would ever
|
|
70
|
-
// run `helm test` to use them; left in, they'd just sit in the namespace as inert clutter.
|
|
71
|
-
const args = ['template', release, `${repoName}/${chart.name}`, '-f', tmpValues, '-n', namespace, '--no-hooks'];
|
|
72
|
-
if (chart.version) args.push('--version', chart.version);
|
|
73
|
-
const res = capture('helm', args);
|
|
74
|
-
if (res.status !== 0) {
|
|
75
|
-
throw new Error(`helm template failed for chart "${chart.name}":\n${res.stderr}`);
|
|
76
|
-
}
|
|
77
|
-
return res.stdout;
|
|
78
|
-
} finally {
|
|
79
|
-
fs.rmSync(tmpValues, { force: true });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Every chart names its Service differently (Bitnami's own "dedupe release name vs chart name"
|
|
84
|
-
// logic alone means postgresql uses `<release>-postgresql` but redis uses `<release>-master`, no
|
|
85
|
-
// chart name at all, when the release name already contains "redis") - predicting it from a
|
|
86
|
-
// naming convention is fragile per-chart guesswork. Reading it back out of the chart's own
|
|
87
|
-
// rendered output is not: whatever the chart actually created is, by definition, correct.
|
|
88
|
-
function findPrimaryService(manifestYaml) {
|
|
89
|
-
const docs = yaml.loadAll(manifestYaml).filter(Boolean);
|
|
90
|
-
const services = docs.filter((d) => d.kind === 'Service');
|
|
91
|
-
return services.find((s) => !/-headless$|-hl$/.test(s.metadata.name)) || services[0] || null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function findServiceHost(manifestYaml) {
|
|
95
|
-
const svc = findPrimaryService(manifestYaml);
|
|
96
|
-
return svc ? svc.metadata.name : null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Same "read it back, don't guess" reasoning as findServiceHost - a wait-for-dependency init
|
|
100
|
-
// container (manifestGen.js) needs a real port, and addon.yaml's own `env` block is free to name
|
|
101
|
-
// its port var anything (POSTGRES_PORT, REDIS_PORT, ...), so it's not something to parse generically.
|
|
102
|
-
function findServicePort(manifestYaml) {
|
|
103
|
-
const svc = findPrimaryService(manifestYaml);
|
|
104
|
-
const port = svc && svc.spec.ports && svc.spec.ports[0];
|
|
105
|
-
return port ? port.port : null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// A small tracking ConfigMap bundled into the addon's own manifest output, so its lifecycle is
|
|
109
|
-
// tied to the addon's (created and deleted together, no separate cleanup code needed). This is
|
|
110
|
-
// what `localctl addons status` reads to list every addon instance in the cluster - it doesn't
|
|
111
|
-
// try to guess which workload kind(s) a given chart happens to use (Deployment, StatefulSet,
|
|
112
|
-
// DaemonSet all show up in real charts; guessing which is exactly the fragility that bit
|
|
113
|
-
// `localctl status`'s own dependency listing once already).
|
|
114
|
-
function buildAddonRecord({ release, namespace, addonName, chart, owner }) {
|
|
115
|
-
return {
|
|
116
|
-
apiVersion: 'v1',
|
|
117
|
-
kind: 'ConfigMap',
|
|
118
|
-
metadata: {
|
|
119
|
-
name: `${release}-localctl-addon`,
|
|
120
|
-
namespace,
|
|
121
|
-
labels: { 'localctl.dev/addon': 'true' },
|
|
122
|
-
},
|
|
123
|
-
data: {
|
|
124
|
-
addon: addonName,
|
|
125
|
-
chart: chart.name,
|
|
126
|
-
version: String(chart.version || ''),
|
|
127
|
-
owner: owner || '',
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Loads one addon (given its folder) and returns the rendered manifest YAML, plus any env vars
|
|
133
|
-
// it wants injected into the caller's own container (per-app addons only - see manifestGen.js).
|
|
134
|
-
function loadAddon(addonDir, { release, namespace, addonName, owner, extraVars = {}, chartVersionOverride }) {
|
|
135
|
-
const specPath = path.join(addonDir, 'addon.yaml');
|
|
136
|
-
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
137
|
-
|
|
138
|
-
const chart = { ...spec.chart, version: chartVersionOverride || spec.chart.version };
|
|
139
|
-
const vars = { release, namespace, ...extraVars };
|
|
140
|
-
|
|
141
|
-
if (spec.secrets) {
|
|
142
|
-
const secretName = `${release}-secret`;
|
|
143
|
-
ensureSecret(secretName, namespace, Object.keys(spec.secrets));
|
|
144
|
-
vars.secretName = secretName;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const configDir = path.join(addonDir, 'config');
|
|
148
|
-
if (spec.config && fs.existsSync(configDir)) {
|
|
149
|
-
const configMapName = `${release}-config`;
|
|
150
|
-
ensureConfigMap(configMapName, namespace, configDir);
|
|
151
|
-
vars.configMapName = configMapName;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const valuesYaml = substitute(spec.values, vars);
|
|
155
|
-
const valuesObj = yaml.load(valuesYaml) || {};
|
|
156
|
-
const manifestYaml = helmTemplate(release, chart, valuesObj, namespace);
|
|
157
|
-
vars.serviceHost = findServiceHost(manifestYaml);
|
|
158
|
-
const servicePort = findServicePort(manifestYaml);
|
|
159
|
-
|
|
160
|
-
const env = {};
|
|
161
|
-
if (spec.env) {
|
|
162
|
-
for (const [k, v] of Object.entries(spec.env)) {
|
|
163
|
-
env[k] = substitute(v, vars);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Unlike `env` (plain values), these become a real k8s secretKeyRef in the app's Deployment -
|
|
168
|
-
// the actual password never passes through a plaintext env value, config file, or `helm` value.
|
|
169
|
-
const envFromSecret = {};
|
|
170
|
-
if (spec.envFromSecret) {
|
|
171
|
-
for (const [k, ref] of Object.entries(spec.envFromSecret)) {
|
|
172
|
-
envFromSecret[k] = { secret: substitute(ref.secret, vars), key: substitute(ref.key, vars) };
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const record = buildAddonRecord({ release, namespace, addonName: addonName || path.basename(addonDir), chart, owner });
|
|
177
|
-
// trimEnd() + explicit '\n' before the separator: `helm template` output doesn't always end
|
|
178
|
-
// with a trailing newline, and joining on a bare '---\n' in that case glues the separator
|
|
179
|
-
// directly onto the last line's content with no line break, corrupting the YAML.
|
|
180
|
-
const fullManifestYaml = [manifestYaml.trimEnd(), yaml.dump(record).trimEnd()].join('\n---\n');
|
|
181
|
-
|
|
182
|
-
// The addon's real, rendered host+port - not env's POSTGRES_HOST/POSTGRES_PORT (those are only
|
|
183
|
-
// as reliable as each addon.yaml's own naming), so a wait-for-dependency init container
|
|
184
|
-
// (manifestGen.js) can work for any addon generically, built-in or your own.
|
|
185
|
-
const connect = vars.serviceHost && servicePort ? { host: vars.serviceHost, port: servicePort } : null;
|
|
186
|
-
|
|
187
|
-
return { manifestYaml: fullManifestYaml, env, envFromSecret, connect };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
module.exports = { loadAddon, substitute };
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
const { run, capture } = require('./exec');
|
|
7
|
+
|
|
8
|
+
// Generic Helm-based addon loader. An addon is a folder containing an addon.yaml (what chart to
|
|
9
|
+
// pull and how to configure it) and, optionally, a config/ folder (literal files - dashboards,
|
|
10
|
+
// datasources, init scripts - that don't belong in Helm values but need to reach the chart as a
|
|
11
|
+
// ConfigMap). See docs/adding-tools.md for the full addon.yaml reference and a worked example.
|
|
12
|
+
|
|
13
|
+
function repoNameFor(url) {
|
|
14
|
+
return `localctl-${crypto.createHash('md5').update(url).digest('hex').slice(0, 8)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureRepo(url) {
|
|
18
|
+
const name = repoNameFor(url);
|
|
19
|
+
// --force-update: idempotent even if re-pointed at a different URL under the same generated name.
|
|
20
|
+
run('helm', ['repo', 'add', name, url, '--force-update'], { stdio: 'ignore' });
|
|
21
|
+
run('helm', ['repo', 'update', name], { stdio: 'ignore', allowFail: true });
|
|
22
|
+
return name;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function substitute(template, vars) {
|
|
26
|
+
return String(template).replace(/\{\{\s*([\w.]+)\s*\}\}/g, (match, key) => {
|
|
27
|
+
if (!(key in vars)) throw new Error(`Addon references unknown template variable "{{${key}}}"`);
|
|
28
|
+
return vars[key];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureNamespace(ns) {
|
|
33
|
+
if (capture('kubectl', ['get', 'namespace', ns]).status !== 0) {
|
|
34
|
+
run('kubectl', ['create', 'namespace', ns], { stdio: 'ignore', allowFail: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generates each secret's value once and stores it as a real k8s Secret; a re-run that finds the
|
|
39
|
+
// Secret already there leaves it alone, so app data doesn't lose its password out from under it
|
|
40
|
+
// on every `localctl up`.
|
|
41
|
+
function ensureSecret(name, namespace, keys) {
|
|
42
|
+
ensureNamespace(namespace);
|
|
43
|
+
if (capture('kubectl', ['get', 'secret', name, '-n', namespace]).status === 0) return;
|
|
44
|
+
const args = ['create', 'secret', 'generic', name, '-n', namespace];
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
args.push(`--from-literal=${key}=${crypto.randomBytes(16).toString('hex')}`);
|
|
47
|
+
}
|
|
48
|
+
run('kubectl', args, { stdio: 'ignore' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Turns a folder of literal files (dashboards, datasource definitions, init scripts - whatever
|
|
52
|
+
// the chart expects to mount, not a Helm value) into a real ConfigMap.
|
|
53
|
+
function ensureConfigMap(name, namespace, configDir) {
|
|
54
|
+
ensureNamespace(namespace);
|
|
55
|
+
const files = fs.readdirSync(configDir).filter((f) => fs.statSync(path.join(configDir, f)).isFile());
|
|
56
|
+
const args = ['create', 'configmap', name, '-n', namespace, '--dry-run=client', '-o', 'yaml'];
|
|
57
|
+
files.forEach((f) => args.push(`--from-file=${path.join(configDir, f)}`));
|
|
58
|
+
const rendered = capture('kubectl', args);
|
|
59
|
+
if (rendered.status !== 0) throw new Error(`Failed to render ConfigMap for ${configDir}: ${rendered.stderr}`);
|
|
60
|
+
run('kubectl', ['apply', '-f', '-'], { stdio: ['pipe', 'ignore', 'inherit'], input: rendered.stdout });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function helmTemplate(release, chart, valuesObj, namespace) {
|
|
64
|
+
const repoName = ensureRepo(chart.repo);
|
|
65
|
+
const tmpValues = path.join(os.tmpdir(), `localctl-addon-values-${process.pid}-${Date.now()}.yaml`);
|
|
66
|
+
fs.writeFileSync(tmpValues, yaml.dump(valuesObj));
|
|
67
|
+
try {
|
|
68
|
+
// --no-hooks: excludes `helm test` pods and similar hook resources from the rendered output -
|
|
69
|
+
// we apply this YAML with plain `kubectl apply`, not `helm install`, so nothing would ever
|
|
70
|
+
// run `helm test` to use them; left in, they'd just sit in the namespace as inert clutter.
|
|
71
|
+
const args = ['template', release, `${repoName}/${chart.name}`, '-f', tmpValues, '-n', namespace, '--no-hooks'];
|
|
72
|
+
if (chart.version) args.push('--version', chart.version);
|
|
73
|
+
const res = capture('helm', args);
|
|
74
|
+
if (res.status !== 0) {
|
|
75
|
+
throw new Error(`helm template failed for chart "${chart.name}":\n${res.stderr}`);
|
|
76
|
+
}
|
|
77
|
+
return res.stdout;
|
|
78
|
+
} finally {
|
|
79
|
+
fs.rmSync(tmpValues, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Every chart names its Service differently (Bitnami's own "dedupe release name vs chart name"
|
|
84
|
+
// logic alone means postgresql uses `<release>-postgresql` but redis uses `<release>-master`, no
|
|
85
|
+
// chart name at all, when the release name already contains "redis") - predicting it from a
|
|
86
|
+
// naming convention is fragile per-chart guesswork. Reading it back out of the chart's own
|
|
87
|
+
// rendered output is not: whatever the chart actually created is, by definition, correct.
|
|
88
|
+
function findPrimaryService(manifestYaml) {
|
|
89
|
+
const docs = yaml.loadAll(manifestYaml).filter(Boolean);
|
|
90
|
+
const services = docs.filter((d) => d.kind === 'Service');
|
|
91
|
+
return services.find((s) => !/-headless$|-hl$/.test(s.metadata.name)) || services[0] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findServiceHost(manifestYaml) {
|
|
95
|
+
const svc = findPrimaryService(manifestYaml);
|
|
96
|
+
return svc ? svc.metadata.name : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Same "read it back, don't guess" reasoning as findServiceHost - a wait-for-dependency init
|
|
100
|
+
// container (manifestGen.js) needs a real port, and addon.yaml's own `env` block is free to name
|
|
101
|
+
// its port var anything (POSTGRES_PORT, REDIS_PORT, ...), so it's not something to parse generically.
|
|
102
|
+
function findServicePort(manifestYaml) {
|
|
103
|
+
const svc = findPrimaryService(manifestYaml);
|
|
104
|
+
const port = svc && svc.spec.ports && svc.spec.ports[0];
|
|
105
|
+
return port ? port.port : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A small tracking ConfigMap bundled into the addon's own manifest output, so its lifecycle is
|
|
109
|
+
// tied to the addon's (created and deleted together, no separate cleanup code needed). This is
|
|
110
|
+
// what `localctl addons status` reads to list every addon instance in the cluster - it doesn't
|
|
111
|
+
// try to guess which workload kind(s) a given chart happens to use (Deployment, StatefulSet,
|
|
112
|
+
// DaemonSet all show up in real charts; guessing which is exactly the fragility that bit
|
|
113
|
+
// `localctl status`'s own dependency listing once already).
|
|
114
|
+
function buildAddonRecord({ release, namespace, addonName, chart, owner }) {
|
|
115
|
+
return {
|
|
116
|
+
apiVersion: 'v1',
|
|
117
|
+
kind: 'ConfigMap',
|
|
118
|
+
metadata: {
|
|
119
|
+
name: `${release}-localctl-addon`,
|
|
120
|
+
namespace,
|
|
121
|
+
labels: { 'localctl.dev/addon': 'true' },
|
|
122
|
+
},
|
|
123
|
+
data: {
|
|
124
|
+
addon: addonName,
|
|
125
|
+
chart: chart.name,
|
|
126
|
+
version: String(chart.version || ''),
|
|
127
|
+
owner: owner || '',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Loads one addon (given its folder) and returns the rendered manifest YAML, plus any env vars
|
|
133
|
+
// it wants injected into the caller's own container (per-app addons only - see manifestGen.js).
|
|
134
|
+
function loadAddon(addonDir, { release, namespace, addonName, owner, extraVars = {}, chartVersionOverride }) {
|
|
135
|
+
const specPath = path.join(addonDir, 'addon.yaml');
|
|
136
|
+
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
137
|
+
|
|
138
|
+
const chart = { ...spec.chart, version: chartVersionOverride || spec.chart.version };
|
|
139
|
+
const vars = { release, namespace, ...extraVars };
|
|
140
|
+
|
|
141
|
+
if (spec.secrets) {
|
|
142
|
+
const secretName = `${release}-secret`;
|
|
143
|
+
ensureSecret(secretName, namespace, Object.keys(spec.secrets));
|
|
144
|
+
vars.secretName = secretName;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const configDir = path.join(addonDir, 'config');
|
|
148
|
+
if (spec.config && fs.existsSync(configDir)) {
|
|
149
|
+
const configMapName = `${release}-config`;
|
|
150
|
+
ensureConfigMap(configMapName, namespace, configDir);
|
|
151
|
+
vars.configMapName = configMapName;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const valuesYaml = substitute(spec.values, vars);
|
|
155
|
+
const valuesObj = yaml.load(valuesYaml) || {};
|
|
156
|
+
const manifestYaml = helmTemplate(release, chart, valuesObj, namespace);
|
|
157
|
+
vars.serviceHost = findServiceHost(manifestYaml);
|
|
158
|
+
const servicePort = findServicePort(manifestYaml);
|
|
159
|
+
|
|
160
|
+
const env = {};
|
|
161
|
+
if (spec.env) {
|
|
162
|
+
for (const [k, v] of Object.entries(spec.env)) {
|
|
163
|
+
env[k] = substitute(v, vars);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Unlike `env` (plain values), these become a real k8s secretKeyRef in the app's Deployment -
|
|
168
|
+
// the actual password never passes through a plaintext env value, config file, or `helm` value.
|
|
169
|
+
const envFromSecret = {};
|
|
170
|
+
if (spec.envFromSecret) {
|
|
171
|
+
for (const [k, ref] of Object.entries(spec.envFromSecret)) {
|
|
172
|
+
envFromSecret[k] = { secret: substitute(ref.secret, vars), key: substitute(ref.key, vars) };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const record = buildAddonRecord({ release, namespace, addonName: addonName || path.basename(addonDir), chart, owner });
|
|
177
|
+
// trimEnd() + explicit '\n' before the separator: `helm template` output doesn't always end
|
|
178
|
+
// with a trailing newline, and joining on a bare '---\n' in that case glues the separator
|
|
179
|
+
// directly onto the last line's content with no line break, corrupting the YAML.
|
|
180
|
+
const fullManifestYaml = [manifestYaml.trimEnd(), yaml.dump(record).trimEnd()].join('\n---\n');
|
|
181
|
+
|
|
182
|
+
// The addon's real, rendered host+port - not env's POSTGRES_HOST/POSTGRES_PORT (those are only
|
|
183
|
+
// as reliable as each addon.yaml's own naming), so a wait-for-dependency init container
|
|
184
|
+
// (manifestGen.js) can work for any addon generically, built-in or your own.
|
|
185
|
+
const connect = vars.serviceHost && servicePort ? { host: vars.serviceHost, port: servicePort } : null;
|
|
186
|
+
|
|
187
|
+
return { manifestYaml: fullManifestYaml, env, envFromSecret, connect };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { loadAddon, substitute };
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
# Cluster-wide logging addon - deploys Grafana Labs' own official loki-stack chart (Loki +
|
|
2
|
-
# Promtail + Grafana, all in one). Enabled via `localctl addons enable logging`.
|
|
3
|
-
#
|
|
4
|
-
# `config` demonstrates the addon config/ folder feature: any file dropped in ./config/ becomes a
|
|
5
|
-
# ConfigMap ("<release>-config"), and {{configMapName}} below wires it in as a Grafana dashboard.
|
|
6
|
-
# See docs/adding-tools.md.
|
|
7
|
-
|
|
8
|
-
chart:
|
|
9
|
-
repo: https://grafana.github.io/helm-charts
|
|
10
|
-
name: loki-stack
|
|
11
|
-
version: "2.10.3"
|
|
12
|
-
|
|
13
|
-
config: true
|
|
14
|
-
|
|
15
|
-
values: |
|
|
16
|
-
loki:
|
|
17
|
-
enabled: true
|
|
18
|
-
promtail:
|
|
19
|
-
enabled: true
|
|
20
|
-
grafana:
|
|
21
|
-
enabled: true
|
|
22
|
-
adminUser: admin
|
|
23
|
-
dashboardProviders:
|
|
24
|
-
dashboardproviders.yaml:
|
|
25
|
-
apiVersion: 1
|
|
26
|
-
providers:
|
|
27
|
-
- name: default
|
|
28
|
-
orgId: 1
|
|
29
|
-
folder: ""
|
|
30
|
-
type: file
|
|
31
|
-
disableDeletion: false
|
|
32
|
-
editable: true
|
|
33
|
-
options:
|
|
34
|
-
path: /var/lib/grafana/dashboards/default
|
|
35
|
-
dashboardsConfigMaps:
|
|
36
|
-
default: "{{configMapName}}"
|
|
37
|
-
ingress:
|
|
38
|
-
enabled: true
|
|
39
|
-
ingressClassName: traefik
|
|
40
|
-
annotations:
|
|
41
|
-
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
|
42
|
-
hosts:
|
|
43
|
-
- "grafana.{{domain}}"
|
|
44
|
-
tls:
|
|
45
|
-
- hosts:
|
|
46
|
-
- "grafana.{{domain}}"
|
|
1
|
+
# Cluster-wide logging addon - deploys Grafana Labs' own official loki-stack chart (Loki +
|
|
2
|
+
# Promtail + Grafana, all in one). Enabled via `localctl addons enable logging`.
|
|
3
|
+
#
|
|
4
|
+
# `config` demonstrates the addon config/ folder feature: any file dropped in ./config/ becomes a
|
|
5
|
+
# ConfigMap ("<release>-config"), and {{configMapName}} below wires it in as a Grafana dashboard.
|
|
6
|
+
# See docs/adding-tools.md.
|
|
7
|
+
|
|
8
|
+
chart:
|
|
9
|
+
repo: https://grafana.github.io/helm-charts
|
|
10
|
+
name: loki-stack
|
|
11
|
+
version: "2.10.3"
|
|
12
|
+
|
|
13
|
+
config: true
|
|
14
|
+
|
|
15
|
+
values: |
|
|
16
|
+
loki:
|
|
17
|
+
enabled: true
|
|
18
|
+
promtail:
|
|
19
|
+
enabled: true
|
|
20
|
+
grafana:
|
|
21
|
+
enabled: true
|
|
22
|
+
adminUser: admin
|
|
23
|
+
dashboardProviders:
|
|
24
|
+
dashboardproviders.yaml:
|
|
25
|
+
apiVersion: 1
|
|
26
|
+
providers:
|
|
27
|
+
- name: default
|
|
28
|
+
orgId: 1
|
|
29
|
+
folder: ""
|
|
30
|
+
type: file
|
|
31
|
+
disableDeletion: false
|
|
32
|
+
editable: true
|
|
33
|
+
options:
|
|
34
|
+
path: /var/lib/grafana/dashboards/default
|
|
35
|
+
dashboardsConfigMaps:
|
|
36
|
+
default: "{{configMapName}}"
|
|
37
|
+
ingress:
|
|
38
|
+
enabled: true
|
|
39
|
+
ingressClassName: traefik
|
|
40
|
+
annotations:
|
|
41
|
+
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
|
42
|
+
hosts:
|
|
43
|
+
- "grafana.{{domain}}"
|
|
44
|
+
tls:
|
|
45
|
+
- hosts:
|
|
46
|
+
- "grafana.{{domain}}"
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"title": "All Logs",
|
|
3
|
-
"uid": "localctl-all-logs",
|
|
4
|
-
"schemaVersion": 39,
|
|
5
|
-
"timezone": "browser",
|
|
6
|
-
"time": { "from": "now-1h", "to": "now" },
|
|
7
|
-
"panels": [
|
|
8
|
-
{
|
|
9
|
-
"type": "logs",
|
|
10
|
-
"title": "Every pod, every namespace",
|
|
11
|
-
"gridPos": { "h": 20, "w": 24, "x": 0, "y": 0 },
|
|
12
|
-
"datasource": { "type": "loki", "uid": "Loki" },
|
|
13
|
-
"targets": [
|
|
14
|
-
{
|
|
15
|
-
"expr": "{namespace=~\".+\"}",
|
|
16
|
-
"refId": "A"
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"title": "All Logs",
|
|
3
|
+
"uid": "localctl-all-logs",
|
|
4
|
+
"schemaVersion": 39,
|
|
5
|
+
"timezone": "browser",
|
|
6
|
+
"time": { "from": "now-1h", "to": "now" },
|
|
7
|
+
"panels": [
|
|
8
|
+
{
|
|
9
|
+
"type": "logs",
|
|
10
|
+
"title": "Every pod, every namespace",
|
|
11
|
+
"gridPos": { "h": 20, "w": 24, "x": 0, "y": 0 },
|
|
12
|
+
"datasource": { "type": "loki", "uid": "Loki" },
|
|
13
|
+
"targets": [
|
|
14
|
+
{
|
|
15
|
+
"expr": "{namespace=~\".+\"}",
|
|
16
|
+
"refId": "A"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# Cluster-wide metrics addon - deploys the real prometheus-community Prometheus chart (server +
|
|
2
|
-
# kube-state-metrics + node-exporter). Enabled via `localctl addons enable monitoring`.
|
|
3
|
-
#
|
|
4
|
-
# No bundled Grafana here (the `logging` addon already brings one) - point it at this Prometheus
|
|
5
|
-
# instance yourself as a datasource if you want dashboards
|
|
6
|
-
# (http://monitoring-prometheus-server.observability.svc.cluster.local - the chart's own Service
|
|
7
|
-
# name, check `kubectl get svc -n observability` if it ever renames itself in a future chart
|
|
8
|
-
# version rather than trusting this comment). Keeping the two addons independent means either
|
|
9
|
-
# works without the other.
|
|
10
|
-
|
|
11
|
-
chart:
|
|
12
|
-
repo: https://prometheus-community.github.io/helm-charts
|
|
13
|
-
name: prometheus
|
|
14
|
-
version: "29.34.0"
|
|
15
|
-
|
|
16
|
-
values: |
|
|
17
|
-
alertmanager:
|
|
18
|
-
enabled: false
|
|
19
|
-
prometheus-pushgateway:
|
|
20
|
-
enabled: false
|
|
21
|
-
server:
|
|
22
|
-
persistentVolume:
|
|
23
|
-
size: 2Gi
|
|
24
|
-
ingress:
|
|
25
|
-
enabled: true
|
|
26
|
-
ingressClassName: traefik
|
|
27
|
-
annotations:
|
|
28
|
-
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
|
29
|
-
hosts:
|
|
30
|
-
- "monitoring.{{domain}}"
|
|
31
|
-
tls:
|
|
32
|
-
- hosts:
|
|
33
|
-
- "monitoring.{{domain}}"
|
|
1
|
+
# Cluster-wide metrics addon - deploys the real prometheus-community Prometheus chart (server +
|
|
2
|
+
# kube-state-metrics + node-exporter). Enabled via `localctl addons enable monitoring`.
|
|
3
|
+
#
|
|
4
|
+
# No bundled Grafana here (the `logging` addon already brings one) - point it at this Prometheus
|
|
5
|
+
# instance yourself as a datasource if you want dashboards
|
|
6
|
+
# (http://monitoring-prometheus-server.observability.svc.cluster.local - the chart's own Service
|
|
7
|
+
# name, check `kubectl get svc -n observability` if it ever renames itself in a future chart
|
|
8
|
+
# version rather than trusting this comment). Keeping the two addons independent means either
|
|
9
|
+
# works without the other.
|
|
10
|
+
|
|
11
|
+
chart:
|
|
12
|
+
repo: https://prometheus-community.github.io/helm-charts
|
|
13
|
+
name: prometheus
|
|
14
|
+
version: "29.34.0"
|
|
15
|
+
|
|
16
|
+
values: |
|
|
17
|
+
alertmanager:
|
|
18
|
+
enabled: false
|
|
19
|
+
prometheus-pushgateway:
|
|
20
|
+
enabled: false
|
|
21
|
+
server:
|
|
22
|
+
persistentVolume:
|
|
23
|
+
size: 2Gi
|
|
24
|
+
ingress:
|
|
25
|
+
enabled: true
|
|
26
|
+
ingressClassName: traefik
|
|
27
|
+
annotations:
|
|
28
|
+
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
|
29
|
+
hosts:
|
|
30
|
+
- "monitoring.{{domain}}"
|
|
31
|
+
tls:
|
|
32
|
+
- hosts:
|
|
33
|
+
- "monitoring.{{domain}}"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# Mongo addon - deploys the real Bitnami MongoDB chart. Auth disabled by default, matching this
|
|
2
|
-
# addon's old behavior (local dev only, not meant to hold anything sensitive) - set auth.enabled
|
|
3
|
-
# and add a secrets/envFromSecret block here (see postgres/addon.yaml) if you want it.
|
|
4
|
-
|
|
5
|
-
chart:
|
|
6
|
-
repo: https://charts.bitnami.com/bitnami
|
|
7
|
-
name: mongodb
|
|
8
|
-
version: "19.3.1"
|
|
9
|
-
|
|
10
|
-
env:
|
|
11
|
-
MONGO_HOST: "{{serviceHost}}"
|
|
12
|
-
MONGO_PORT: "27017"
|
|
13
|
-
|
|
14
|
-
values: |
|
|
15
|
-
auth:
|
|
16
|
-
enabled: false
|
|
17
|
-
persistence:
|
|
18
|
-
size: "{{storage}}"
|
|
1
|
+
# Mongo addon - deploys the real Bitnami MongoDB chart. Auth disabled by default, matching this
|
|
2
|
+
# addon's old behavior (local dev only, not meant to hold anything sensitive) - set auth.enabled
|
|
3
|
+
# and add a secrets/envFromSecret block here (see postgres/addon.yaml) if you want it.
|
|
4
|
+
|
|
5
|
+
chart:
|
|
6
|
+
repo: https://charts.bitnami.com/bitnami
|
|
7
|
+
name: mongodb
|
|
8
|
+
version: "19.3.1"
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
MONGO_HOST: "{{serviceHost}}"
|
|
12
|
+
MONGO_PORT: "27017"
|
|
13
|
+
|
|
14
|
+
values: |
|
|
15
|
+
auth:
|
|
16
|
+
enabled: false
|
|
17
|
+
persistence:
|
|
18
|
+
size: "{{storage}}"
|