@platformatic/runtime 4.0.0-new-config.2 → 4.0.0-new-config.6
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/config.d.ts +594 -596
- package/index.d.ts +11 -5
- package/index.js +161 -85
- package/lib/config.js +130 -237
- package/lib/errors.js +12 -0
- package/lib/generator.js +222 -35
- package/lib/logger.js +3 -3
- package/lib/management-api.js +4 -60
- package/lib/policies.js +27 -0
- package/lib/runtime.js +315 -239
- package/lib/schema.js +76 -0
- package/lib/worker/controller.js +54 -49
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +49 -2
- package/lib/worker/main.js +28 -53
- package/lib/worker/symbols.js +1 -1
- package/package.json +18 -18
- package/schema.json +87 -44
package/lib/generator.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
+
import { createEnvFileTool, defaultPackageManager, generateDashedName, safeRemove } from '@platformatic/foundation'
|
|
2
|
+
import { LegacyConfigurationFileError, serializeConfiguration } from '@platformatic/foundation/lib/v4/index.js'
|
|
1
3
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from '@platformatic/
|
|
11
|
-
import { BaseGenerator, envObjectToString, getApplicationTemplateFromSchemaUrl } from '@platformatic/generators'
|
|
4
|
+
BaseGenerator,
|
|
5
|
+
appendApplications,
|
|
6
|
+
envObjectToString,
|
|
7
|
+
findAnyConfigurationFile,
|
|
8
|
+
listedApplications,
|
|
9
|
+
rawSource,
|
|
10
|
+
readEnvFile,
|
|
11
|
+
getApplicationTemplateFromSchemaUrl
|
|
12
|
+
} from '@platformatic/generators'
|
|
12
13
|
import { existsSync } from 'node:fs'
|
|
13
14
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
14
15
|
import { createRequire } from 'node:module'
|
|
15
|
-
import { basename, join } from 'node:path'
|
|
16
|
+
import { basename, join, resolve, sep } from 'node:path'
|
|
16
17
|
import { pathToFileURL } from 'node:url'
|
|
17
|
-
import { transform } from './config.js'
|
|
18
|
-
import { schema } from './schema.js'
|
|
19
18
|
import { getArrayDifference } from './utils.js'
|
|
20
19
|
|
|
21
20
|
const engines = {
|
|
@@ -38,6 +37,11 @@ function getRuntimeBaseEnvVars (config) {
|
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
/*
|
|
41
|
+
The one thing an import changes about a root: which applications it lists. Applied to the module
|
|
42
|
+
the user has rather than to a re-rendering of it, so their formatting, their comments and the
|
|
43
|
+
expressions their values are written as all survive the edit.
|
|
44
|
+
*/
|
|
41
45
|
export class RuntimeGenerator extends BaseGenerator {
|
|
42
46
|
constructor (opts) {
|
|
43
47
|
super({
|
|
@@ -74,6 +78,7 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
74
78
|
async generatePackageJson () {
|
|
75
79
|
const template = {
|
|
76
80
|
name: `${this.runtimeName}`,
|
|
81
|
+
type: 'module',
|
|
77
82
|
scripts: {
|
|
78
83
|
dev: this.config.devCommand ?? 'wattpm dev',
|
|
79
84
|
build: this.config.buildCommand ?? 'wattpm build',
|
|
@@ -120,17 +125,46 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
120
125
|
return
|
|
121
126
|
}
|
|
122
127
|
this._hasCheckedForExistingConfig = true
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
ignoreProcessEnv: true
|
|
129
|
-
})
|
|
128
|
+
/*
|
|
129
|
+
Whichever dialect the project is in: a v4 configuration is a module and is invisible to the
|
|
130
|
+
legacy lookup, so without this the wizard treats a project it scaffolded itself as a new one.
|
|
131
|
+
*/
|
|
132
|
+
const existingConfigFile = await findAnyConfigurationFile(this.targetDirectory)
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
this.
|
|
133
|
-
|
|
134
|
+
if (existingConfigFile && existsSync(join(this.targetDirectory, existingConfigFile))) {
|
|
135
|
+
const existingConfigPath = join(this.targetDirectory, existingConfigFile)
|
|
136
|
+
|
|
137
|
+
this.existingConfigRaw = await this.readConfigurationFile(existingConfigPath, 'root', this.targetDirectory)
|
|
138
|
+
/*
|
|
139
|
+
The file as written, kept so that editing it can be an edit. Re-emitting from the evaluated
|
|
140
|
+
configuration would bake this machine's environment into the user's root -- a reference to
|
|
141
|
+
PLT_SERVER_LOGGER_LEVEL becomes whatever it happens to resolve to here -- and take every
|
|
142
|
+
comment with it.
|
|
143
|
+
*/
|
|
144
|
+
this.existingConfigFile = existingConfigFile
|
|
145
|
+
this.existingConfigSource = await readFile(existingConfigPath, 'utf-8')
|
|
146
|
+
|
|
147
|
+
/*
|
|
148
|
+
A legacy root is refused with the hint every other v4 entry point gives, rather than loaded
|
|
149
|
+
and rewritten: continuing wrote the v4 module form over the .json file, and migrate is the
|
|
150
|
+
tool whose whole job is that conversion -- with the refusals and divergence reports the
|
|
151
|
+
wizard has no way to make.
|
|
152
|
+
*/
|
|
153
|
+
if (existingConfigFile.endsWith('.json')) {
|
|
154
|
+
throw new LegacyConfigurationFileError(existingConfigPath)
|
|
155
|
+
} else {
|
|
156
|
+
/*
|
|
157
|
+
Derived from the root rather than loaded through it. A full load evaluates every
|
|
158
|
+
application's configuration, and those import their capabilities -- so the wizard would
|
|
159
|
+
need the project's dependencies installed before it could read the project, which is
|
|
160
|
+
exactly the state it is called in to fix.
|
|
161
|
+
*/
|
|
162
|
+
const { PLT_ROOT, ...existingEnvironment } = await readEnvFile(this.targetDirectory)
|
|
163
|
+
|
|
164
|
+
this.config.env = existingEnvironment
|
|
165
|
+
this.existingConfig = this.existingConfigRaw
|
|
166
|
+
this.existingApplications = await this.#listExistingApplications(this.existingConfigRaw)
|
|
167
|
+
}
|
|
134
168
|
|
|
135
169
|
this.updateRuntimeConfig(this.existingConfigRaw)
|
|
136
170
|
this.updateRuntimeEnv(await readFile(join(this.targetDirectory, '.env'), 'utf-8'))
|
|
@@ -154,6 +188,15 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
154
188
|
|
|
155
189
|
setApplicationsConfigValues () {
|
|
156
190
|
let newApplicationOrdinal = 0
|
|
191
|
+
// A runtime of exactly one application has no mesh of siblings to fall back on: with the v4
|
|
192
|
+
// entrypoint gone, a sole application that declares no port would bind nothing and be reachable
|
|
193
|
+
// from nowhere. So the sole application is marked as the entrypoint here -- the one place the
|
|
194
|
+
// whole application set is known -- and a capability that otherwise scaffolds no port reads the
|
|
195
|
+
// flag and exposes itself on PORT/3042 (@platformatic/node does; the framework capabilities,
|
|
196
|
+
// which also scaffold no port, can adopt the same flag). The count is the gate: the moment a
|
|
197
|
+
// second application exists, nothing is forced and a portless application stays mesh-only, which
|
|
198
|
+
// is what keeps "expose the one" from becoming "expose them all".
|
|
199
|
+
const totalApplications = this.existingApplications.length + this.applications.length
|
|
157
200
|
this.applications.forEach(({ name, application }) => {
|
|
158
201
|
if (!application.config) {
|
|
159
202
|
// set default config
|
|
@@ -162,6 +205,7 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
162
205
|
const existingOrdinal = this.existingApplications.indexOf(name)
|
|
163
206
|
const ordinal = existingOrdinal === -1 ? this.existingApplications.length + newApplicationOrdinal++ : existingOrdinal
|
|
164
207
|
this.setApplicationPort(application, ordinal)
|
|
208
|
+
application.config.entrypoint = totalApplications === 1
|
|
165
209
|
})
|
|
166
210
|
}
|
|
167
211
|
|
|
@@ -171,6 +215,33 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
171
215
|
}
|
|
172
216
|
}
|
|
173
217
|
|
|
218
|
+
/*
|
|
219
|
+
The root is spelled the same way an application is: it imports `defineConfig` from wattpm and
|
|
220
|
+
calls it, so the editor types and checks the shape the loader would otherwise only reject at
|
|
221
|
+
boot. The import means the file cannot be evaluated before its dependencies are installed --
|
|
222
|
+
which is why `wattpm install`/`create` install the root's own dependencies before reading it to
|
|
223
|
+
discover the applications (see installDependencies in wattpm-utils).
|
|
224
|
+
*/
|
|
225
|
+
serializeConfigFile (config) {
|
|
226
|
+
/*
|
|
227
|
+
Unstamped while this line is still 3.x. The marker is a version declaration, and the loader
|
|
228
|
+
refuses a v3 one outright -- correctly, since that is how it catches a configuration nobody
|
|
229
|
+
migrated. It becomes writable, and worth writing, at 4.0.0.
|
|
230
|
+
*/
|
|
231
|
+
const { $schema, module: _module, ...rest } = config
|
|
232
|
+
// Resolved like every other scaffolded value: the root's placeholders are the same kind of
|
|
233
|
+
// thing, and leaving them would write the placeholder's own text where an expression belongs.
|
|
234
|
+
const resolved = this.resolveScaffoldedPlaceholders(rest)
|
|
235
|
+
|
|
236
|
+
/*
|
|
237
|
+
The root is spelled the same way an application is: a factory call, `defineConfig` from
|
|
238
|
+
wattpm, whose whole job is to type its argument so the editor completes and checks the shape
|
|
239
|
+
the loader would otherwise only reject at boot. wattpm's defineConfig imports nothing, so the
|
|
240
|
+
root evaluates in the eval worker without paying for the runtime it would otherwise pull in.
|
|
241
|
+
*/
|
|
242
|
+
return `import { defineConfig } from 'wattpm'\n\nexport default defineConfig(${serializeConfiguration(resolved)})\n`
|
|
243
|
+
}
|
|
244
|
+
|
|
174
245
|
async _getConfigFileContents () {
|
|
175
246
|
const config = {
|
|
176
247
|
$schema: `https://schemas.platformatic.dev/wattpm/${this.platformaticVersion}.json`,
|
|
@@ -236,7 +307,11 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
236
307
|
}
|
|
237
308
|
let basePath
|
|
238
309
|
if (this.existingConfig) {
|
|
239
|
-
|
|
310
|
+
/*
|
|
311
|
+
Resolved against the project, because the configuration says it relative to itself. The v3
|
|
312
|
+
loader handed back an absolute path; reading the file directly hands back what it says.
|
|
313
|
+
*/
|
|
314
|
+
basePath = resolve(this.targetDirectory, this.existingConfig.autoload.path)
|
|
240
315
|
} else {
|
|
241
316
|
basePath = join(this.targetDirectory, this.config.autoload || this.applicationsFolder)
|
|
242
317
|
}
|
|
@@ -287,11 +362,38 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
287
362
|
return (await import(pathToFileURL(fileToImport))).Generator
|
|
288
363
|
}
|
|
289
364
|
|
|
365
|
+
/*
|
|
366
|
+
Which applications a root already describes: the entries it lists, and the directories its
|
|
367
|
+
autoload would discover. Neither needs an application's own configuration, which is the point.
|
|
368
|
+
*/
|
|
369
|
+
async #listExistingApplications (config) {
|
|
370
|
+
const ids = (config.applications ?? config.services ?? config.web ?? []).map(entry => entry.id).filter(Boolean)
|
|
371
|
+
|
|
372
|
+
if (!config.autoload?.path) {
|
|
373
|
+
return ids
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const exclude = config.autoload.exclude ?? []
|
|
377
|
+
const directory = join(this.targetDirectory, config.autoload.path)
|
|
378
|
+
|
|
379
|
+
for (const entry of await readdir(directory, { withFileTypes: true }).catch(() => [])) {
|
|
380
|
+
if (entry.isDirectory() && !exclude.includes(entry.name) && !ids.includes(entry.name)) {
|
|
381
|
+
ids.push(entry.name)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return ids
|
|
386
|
+
}
|
|
387
|
+
|
|
290
388
|
async loadFromDir () {
|
|
291
389
|
const output = {
|
|
292
390
|
applications: []
|
|
293
391
|
}
|
|
294
|
-
const runtimePkgConfigFileData =
|
|
392
|
+
const runtimePkgConfigFileData = await this.readConfigurationFile(
|
|
393
|
+
join(this.targetDirectory, await findAnyConfigurationFile(this.targetDirectory)),
|
|
394
|
+
'root',
|
|
395
|
+
this.targetDirectory
|
|
396
|
+
)
|
|
295
397
|
const applicationsPath = join(this.targetDirectory, runtimePkgConfigFileData.autoload.path)
|
|
296
398
|
|
|
297
399
|
// load all applications
|
|
@@ -302,8 +404,14 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
302
404
|
const dirStat = await stat(currentApplicationPath)
|
|
303
405
|
if (dirStat.isDirectory()) {
|
|
304
406
|
// load the application config
|
|
305
|
-
const configFile = await
|
|
306
|
-
|
|
407
|
+
const configFile = await findAnyConfigurationFile(currentApplicationPath)
|
|
408
|
+
// The environment is the runtime's: an application's expressions read the project's .env,
|
|
409
|
+
// which lives at the root beside the configuration that autoloads it.
|
|
410
|
+
const applicationPltJson = await this.readConfigurationFile(
|
|
411
|
+
join(currentApplicationPath, configFile),
|
|
412
|
+
'application',
|
|
413
|
+
this.targetDirectory
|
|
414
|
+
)
|
|
307
415
|
// get module to load
|
|
308
416
|
const template = applicationPltJson.module || getApplicationTemplateFromSchemaUrl(applicationPltJson.$schema)
|
|
309
417
|
const Generator = await this._getGeneratorForTemplate(currentApplicationPath, template)
|
|
@@ -350,8 +458,12 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
350
458
|
|
|
351
459
|
// delete dependencies
|
|
352
460
|
const applicationPath = join(this.targetDirectory, this.applicationsFolder, s.name)
|
|
353
|
-
const configFile = await
|
|
354
|
-
const applicationPackageJson =
|
|
461
|
+
const configFile = await findAnyConfigurationFile(applicationPath)
|
|
462
|
+
const applicationPackageJson = await this.readConfigurationFile(
|
|
463
|
+
join(applicationPath, configFile),
|
|
464
|
+
'application',
|
|
465
|
+
this.targetDirectory
|
|
466
|
+
)
|
|
355
467
|
if (applicationPackageJson.plugins && applicationPackageJson.plugins.packages) {
|
|
356
468
|
applicationPackageJson.plugins.packages.forEach(p => {
|
|
357
469
|
delete currrentPackageJson.dependencies[p.name]
|
|
@@ -468,14 +580,67 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
468
580
|
}
|
|
469
581
|
|
|
470
582
|
updateRuntimeConfig (config) {
|
|
583
|
+
this.generatedConfig = config
|
|
584
|
+
|
|
585
|
+
// Editing what is there, rather than replacing it with a rendering of what it evaluated to.
|
|
586
|
+
if (this.existingConfigSource && !this.existingConfigFile.endsWith('.json')) {
|
|
587
|
+
this.addFile({
|
|
588
|
+
path: '',
|
|
589
|
+
file: this.existingConfigFile,
|
|
590
|
+
contents: this.#editExistingRoot(config),
|
|
591
|
+
tags: ['runtime-config']
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
471
597
|
this.addFile({
|
|
472
598
|
path: '',
|
|
473
|
-
file: this.
|
|
474
|
-
contents:
|
|
599
|
+
file: this.existingConfigFile ?? this.configurationFileName(),
|
|
600
|
+
contents: this.serializeConfigFile(config),
|
|
475
601
|
tags: ['runtime-config']
|
|
476
602
|
})
|
|
477
603
|
}
|
|
478
604
|
|
|
605
|
+
/*
|
|
606
|
+
An existing root is edited rather than rewritten, so that everything it says survives -- see
|
|
607
|
+
`appendApplications`. A shape it cannot edit is reported here rather than papered over by
|
|
608
|
+
writing the file from the loaded configuration, which would be a silent rewrite of every
|
|
609
|
+
reference the root contains.
|
|
610
|
+
*/
|
|
611
|
+
#editExistingRoot (config) {
|
|
612
|
+
/*
|
|
613
|
+
The evaluated configuration arrives with autoload already expanded, so its application list
|
|
614
|
+
holds every discovered directory as an explicit entry with an absolute machine path.
|
|
615
|
+
Appending those would bake this machine's layout into the user's root -- and duplicate what
|
|
616
|
+
autoload will discover again on the next boot. Only entries autoload does not cover belong in
|
|
617
|
+
the file: the ones this run scaffolds outside the autoload directory, or a root with no
|
|
618
|
+
autoload at all.
|
|
619
|
+
*/
|
|
620
|
+
const autoloadPath = config.autoload?.path
|
|
621
|
+
? resolve(this.targetDirectory, config.autoload.path) + sep
|
|
622
|
+
: null
|
|
623
|
+
const additions = listedApplications(config).filter(entry => {
|
|
624
|
+
if (!autoloadPath || typeof entry.path !== 'string') {
|
|
625
|
+
return true
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return !resolve(this.targetDirectory, entry.path).startsWith(autoloadPath)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
const edited = appendApplications(this.existingConfigSource, additions, entry =>
|
|
632
|
+
this.resolveScaffoldedPlaceholders(entry, rawSource)
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
if (edited === null) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`Cannot add applications to ${this.existingConfigFile}: its default export is not a configuration object this tool can edit. Add them by hand.`
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return edited
|
|
642
|
+
}
|
|
643
|
+
|
|
479
644
|
updateRuntimeEnv (contents) {
|
|
480
645
|
this.addFile({
|
|
481
646
|
path: '',
|
|
@@ -497,6 +662,13 @@ export class WrappedGenerator extends BaseGenerator {
|
|
|
497
662
|
async #updateEnvironment () {
|
|
498
663
|
this.addEnvVars(getRuntimeBaseEnvVars(this.config), { overwrite: false, default: true })
|
|
499
664
|
|
|
665
|
+
// A wrapped project is a runtime of one application by construction, so that application is the
|
|
666
|
+
// one that must face the network -- otherwise the wrap would leave the code it was built around
|
|
667
|
+
// reachable from nowhere. The port is registered here so #createConfigFile can read it back as
|
|
668
|
+
// `Number(process.env.PORT || 3042)`; bare PORT rather than a scoped name, because the wrapped
|
|
669
|
+
// application is the root's own singular one and is what a hosting platform's PORT addresses.
|
|
670
|
+
this.addEnvVar('PORT', 3042, { overwrite: false, default: true })
|
|
671
|
+
|
|
500
672
|
this.addFile({
|
|
501
673
|
path: '',
|
|
502
674
|
file: '.env',
|
|
@@ -551,15 +723,30 @@ export class WrappedGenerator extends BaseGenerator {
|
|
|
551
723
|
}
|
|
552
724
|
|
|
553
725
|
async #createConfigFile () {
|
|
726
|
+
/*
|
|
727
|
+
The wrapped single-app root. v3 spelled the runtime settings under a `runtime` key inside the
|
|
728
|
+
application's own configuration; v4 has no such block, so they are the root's own and the
|
|
729
|
+
application is the singular shorthand.
|
|
730
|
+
*/
|
|
731
|
+
/*
|
|
732
|
+
The application is the root's own, named by the singular shorthand. Without it the root
|
|
733
|
+
describes a runtime with nothing in it -- it would load, and start none of the code it was
|
|
734
|
+
wrapped around.
|
|
735
|
+
|
|
736
|
+
Its capability is spelled as a plain object rather than a factory call because this file is
|
|
737
|
+
read before anything is installed, and an import cannot be resolved in that state.
|
|
738
|
+
*/
|
|
554
739
|
const config = {
|
|
555
|
-
|
|
556
|
-
|
|
740
|
+
...this.resolveScaffoldedPlaceholders(getRuntimeWrappableProperties()),
|
|
741
|
+
application: {
|
|
742
|
+
config: this.resolveScaffoldedPlaceholders({ module: this.module, server: { port: '{PORT}' } })
|
|
743
|
+
}
|
|
557
744
|
}
|
|
558
745
|
|
|
559
746
|
this.addFile({
|
|
560
747
|
path: '',
|
|
561
|
-
file:
|
|
562
|
-
contents:
|
|
748
|
+
file: this.configurationFileName(),
|
|
749
|
+
contents: `export default ${serializeConfiguration(config)}\n`
|
|
563
750
|
})
|
|
564
751
|
}
|
|
565
752
|
|
package/lib/logger.js
CHANGED
|
@@ -191,13 +191,13 @@ export async function createLogger (config) {
|
|
|
191
191
|
|
|
192
192
|
const multiStream = pino.multistream([{ stream: cliStream, level: loggerConfig.level }])
|
|
193
193
|
|
|
194
|
-
if (config.
|
|
194
|
+
if (config.tracing && config.logger.openTelemetryExporter) {
|
|
195
195
|
const openTelemetryTransport = pino.transport({
|
|
196
196
|
target: 'pino-opentelemetry-transport',
|
|
197
197
|
options: {
|
|
198
198
|
resourceAttributes: {
|
|
199
|
-
'service.name': config.
|
|
200
|
-
'service.version': config.
|
|
199
|
+
'service.name': config.tracing.applicationName,
|
|
200
|
+
'service.version': config.tracing.version
|
|
201
201
|
},
|
|
202
202
|
logRecordProcessorOptions: [
|
|
203
203
|
{
|
package/lib/management-api.js
CHANGED
|
@@ -99,18 +99,6 @@ export async function managementApiPlugin (app, opts) {
|
|
|
99
99
|
return runtime.getRuntimeMetadata()
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
app.get('/config', quiet, async request => {
|
|
103
|
-
const metadata = request.query.metadata === 'true'
|
|
104
|
-
const rawConfig = await runtime.getRuntimeConfig(metadata)
|
|
105
|
-
|
|
106
|
-
if (metadata) {
|
|
107
|
-
const { [kMetadata]: __metadata, ...config } = rawConfig
|
|
108
|
-
return { ...config, __metadata }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return rawConfig
|
|
112
|
-
})
|
|
113
|
-
|
|
114
102
|
app.get('/env', quiet, async () => {
|
|
115
103
|
return { ...process.env, ...runtime.getRuntimeEnv() }
|
|
116
104
|
})
|
|
@@ -301,7 +289,10 @@ export async function managementApiPlugin (app, opts) {
|
|
|
301
289
|
})
|
|
302
290
|
|
|
303
291
|
app.get('/metrics', quiet, async (req, reply) => {
|
|
304
|
-
|
|
292
|
+
// The live read: this needs one boolean, and the public getter builds a frozen snapshot of the
|
|
293
|
+
// whole configuration -- which is the right shape to hand a consumer and the wrong one to pay
|
|
294
|
+
// for on a scrape.
|
|
295
|
+
const config = runtime.getRuntimeConfig(true)
|
|
305
296
|
|
|
306
297
|
if (config.metrics?.enabled === false) {
|
|
307
298
|
reply.code(501)
|
|
@@ -324,53 +315,6 @@ export async function managementApiPlugin (app, opts) {
|
|
|
324
315
|
return metrics
|
|
325
316
|
})
|
|
326
317
|
|
|
327
|
-
// TODO: Remove in next major version - deprecated endpoint
|
|
328
|
-
app.get('/metrics/live', { ...quiet, websocket: true }, async socket => {
|
|
329
|
-
const config = await runtime.getRuntimeConfig()
|
|
330
|
-
|
|
331
|
-
if (config.metrics?.enabled === false) {
|
|
332
|
-
socket.send(
|
|
333
|
-
JSON.stringify({
|
|
334
|
-
statusCode: 501,
|
|
335
|
-
error: 'Not Implemented',
|
|
336
|
-
message: 'Metrics are disabled.'
|
|
337
|
-
}),
|
|
338
|
-
() => {
|
|
339
|
-
socket.close()
|
|
340
|
-
}
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const pollAndSendMetrics = async () => {
|
|
347
|
-
try {
|
|
348
|
-
const metrics = await runtime.getFormattedMetrics()
|
|
349
|
-
if (metrics) {
|
|
350
|
-
const serializedMetrics = JSON.stringify(metrics)
|
|
351
|
-
socket.send(serializedMetrics + '\n')
|
|
352
|
-
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
// If there's an error, stop polling and close the connection
|
|
355
|
-
clearInterval(pollingInterval)
|
|
356
|
-
socket.close()
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Poll every second
|
|
361
|
-
const pollingInterval = setInterval(pollAndSendMetrics, 1000)
|
|
362
|
-
|
|
363
|
-
// Send initial metrics immediately
|
|
364
|
-
await pollAndSendMetrics()
|
|
365
|
-
|
|
366
|
-
const cleanup = () => {
|
|
367
|
-
clearInterval(pollingInterval)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
socket.on('error', cleanup)
|
|
371
|
-
socket.on('close', cleanup)
|
|
372
|
-
})
|
|
373
|
-
|
|
374
318
|
app.get('/logs/live', { ...quiet, websocket: true }, async socket => {
|
|
375
319
|
runtime.addLoggerDestination(createWebSocketStream(socket))
|
|
376
320
|
})
|
package/lib/policies.js
CHANGED
|
@@ -21,3 +21,30 @@ export function createChannelCreationHook (config) {
|
|
|
21
21
|
return !forbidden.has(`${first.toLowerCase()}:${second.toLowerCase()}`)
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
export function createTargetPermissionHook (config) {
|
|
26
|
+
const denyList = config.policies?.deny
|
|
27
|
+
|
|
28
|
+
if (typeof denyList === 'undefined') {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const forbidden = new Set()
|
|
33
|
+
|
|
34
|
+
for (let [first, unalloweds] of Object.entries(denyList)) {
|
|
35
|
+
for (let second of unalloweds) {
|
|
36
|
+
first = first.toLowerCase()
|
|
37
|
+
second = second.toLowerCase()
|
|
38
|
+
|
|
39
|
+
forbidden.add(`${first}:${second}`)
|
|
40
|
+
forbidden.add(`${second}:${first}`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return function targetPermissionHook (req, target) {
|
|
45
|
+
const source = config.applicationId ?? req?.headers?.['x-plt-application-id']
|
|
46
|
+
const destination = target?.metadata?.applicationId
|
|
47
|
+
|
|
48
|
+
return !source || !destination || !forbidden.has(`${source.toLowerCase()}:${destination.toLowerCase()}`)
|
|
49
|
+
}
|
|
50
|
+
}
|