sla-wizard-plugin-custom-baseurl 1.0.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/.github/workflows/npm-publish.yml +20 -0
- package/LICENSE +674 -0
- package/README.md +404 -0
- package/index.js +101 -0
- package/package.json +18 -0
- package/src/commands.js +32 -0
- package/src/nginx-transform.js +117 -0
- package/src/sanitize.js +9 -0
- package/test-specs/hpc-oas.yaml +246 -0
- package/test-specs/slas/sla_dgalvan_us_es.yaml +31 -0
- package/test-specs/slas/sla_japarejo_us_es.yaml +31 -0
- package/test-specs/slas/sla_pablofm_us_es.yaml +31 -0
- package/tests/cli-with-plugin.js +13 -0
- package/tests/tests.js +826 -0
package/README.md
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# sla-wizard-plugin-custom-baseUrl
|
|
2
|
+
|
|
3
|
+
A plugin for [SLA Wizard](../sla-wizard) and [sla-wizard-nginx-confd](../sla-wizard-nginx-confd) that adds support for the `x-nginx-server-baseurl` OAS extension.
|
|
4
|
+
|
|
5
|
+
When a path declares `x-nginx-server-baseurl`, nginx will proxy that endpoint to the **custom backend URL** instead of the global `servers[0].url`. Endpoints without the extension continue to use the global default.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Client → nginx: POST /models/chatgpt/v1/chat/completions
|
|
9
|
+
nginx → backend: POST http://localhost:8001/v1/chat/completions ← custom URL
|
|
10
|
+
|
|
11
|
+
Client → nginx: POST /models/qwen/v1/chat/completions
|
|
12
|
+
nginx → backend: POST http://localhost:8000/v1/chat/completions ← default URL
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The plugin also inherits full `x-nginx-strip` support (via `sla-wizard-plugin-nginx-strip`), so you can freely combine both extensions on the same OAS path.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
### 1. Annotate your OAS with `x-nginx-server-baseurl`
|
|
22
|
+
|
|
23
|
+
Add the extension at the **path level** (not operation level):
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
# oas.yaml
|
|
27
|
+
servers:
|
|
28
|
+
- url: http://localhost:8000 # default for all endpoints
|
|
29
|
+
|
|
30
|
+
paths:
|
|
31
|
+
/models/qwen/v1/chat/completions:
|
|
32
|
+
post: ... # no extension → proxy_pass http://localhost:8000
|
|
33
|
+
|
|
34
|
+
/models/chatgpt/v1/chat/completions:
|
|
35
|
+
x-nginx-server-baseurl: http://localhost:8001 # overrides proxy_pass
|
|
36
|
+
post: ...
|
|
37
|
+
|
|
38
|
+
/models/claude/v1/chat/completions:
|
|
39
|
+
x-nginx-server-baseurl: http://localhost:8002
|
|
40
|
+
post: ...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Write your SLAs normally
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
# sla.yaml
|
|
47
|
+
plan:
|
|
48
|
+
name: normal
|
|
49
|
+
rates:
|
|
50
|
+
/models/chatgpt/v1/chat/completions:
|
|
51
|
+
post:
|
|
52
|
+
requests:
|
|
53
|
+
- max: 5
|
|
54
|
+
period: minute
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If you combine this plugin with `x-nginx-strip`, your SLAs can reference the backend (stripped) path and the plugin handles the expansion automatically.
|
|
58
|
+
|
|
59
|
+
### 3. Run the plugin
|
|
60
|
+
|
|
61
|
+
The plugin generates nginx configuration where each location block uses the endpoint-specific `proxy_pass` target:
|
|
62
|
+
|
|
63
|
+
```nginx
|
|
64
|
+
# Without x-nginx-server-baseurl (standard output)
|
|
65
|
+
location /sla-alice_normal_modelschatgptv1chatcompletions_POST {
|
|
66
|
+
rewrite /sla-alice_normal_modelschatgptv1chatcompletions_POST $uri_original break;
|
|
67
|
+
proxy_pass http://localhost:8000; ← same URL for every endpoint
|
|
68
|
+
...
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# With x-nginx-server-baseurl applied
|
|
72
|
+
location /sla-alice_normal_modelschatgptv1chatcompletions_POST {
|
|
73
|
+
rewrite /sla-alice_normal_modelschatgptv1chatcompletions_POST $uri_original break;
|
|
74
|
+
proxy_pass http://localhost:8001; ← custom URL for this endpoint only
|
|
75
|
+
...
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd sla-wizard-plugin-custom-baseUrl
|
|
85
|
+
npm install
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The plugin depends on `sla-wizard-plugin-nginx-strip` (bundled as a local dependency).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Usage — CLI
|
|
93
|
+
|
|
94
|
+
Register the plugin in a CLI wrapper and invoke the commands.
|
|
95
|
+
|
|
96
|
+
### CLI wrapper (one-time setup)
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
// my-cli.js
|
|
100
|
+
const slaWizard = require("sla-wizard");
|
|
101
|
+
const customBaseUrlPlugin = require("sla-wizard-plugin-custom-baseUrl");
|
|
102
|
+
|
|
103
|
+
slaWizard.use(customBaseUrlPlugin);
|
|
104
|
+
slaWizard.program.parse(process.argv);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `config-nginx-baseurl` — full config (nginx.conf + conf.d/)
|
|
108
|
+
|
|
109
|
+
Generates the complete nginx configuration split into a main `nginx.conf` and
|
|
110
|
+
per-user `conf.d/` files:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
node my-cli.js config-nginx-baseurl \
|
|
114
|
+
-o ./nginx-output \
|
|
115
|
+
--oas ./specs/oas.yaml \
|
|
116
|
+
--sla ./specs/slas \
|
|
117
|
+
--authName apikey \
|
|
118
|
+
--proxyPort 80
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Output:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
nginx-output/
|
|
125
|
+
├── nginx.conf ← server block + URI routing rules
|
|
126
|
+
└── conf.d/
|
|
127
|
+
├── sla-alice_us.conf
|
|
128
|
+
├── sla-bob_us.conf
|
|
129
|
+
└── ...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each `conf.d/` file contains the rate-limiting zones, API-key map, and location
|
|
133
|
+
blocks for one user. Location blocks for endpoints with `x-nginx-server-baseurl`
|
|
134
|
+
will have the custom URL in their `proxy_pass` directive.
|
|
135
|
+
|
|
136
|
+
### `add-to-baseurl-confd` — conf.d only (no nginx.conf overwrite)
|
|
137
|
+
|
|
138
|
+
Useful for adding a new user or plan without touching the main `nginx.conf`:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
node my-cli.js add-to-baseurl-confd \
|
|
142
|
+
-o ./nginx-output \
|
|
143
|
+
--oas ./specs/oas.yaml \
|
|
144
|
+
--sla ./specs/slas/sla_newuser.yaml
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### CLI options
|
|
148
|
+
|
|
149
|
+
| Option | Description | Default |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `-o, --outDir <dir>` | Output directory | **required** |
|
|
152
|
+
| `--oas <path>` | Path to OAS v3 file | `./specs/oas.yaml` |
|
|
153
|
+
| `--sla <path>` | Single SLA file, directory of SLAs, or URL | `./specs/sla.yaml` |
|
|
154
|
+
| `--authLocation <loc>` | Where to read the API key: `header`, `query`, `url` | `header` |
|
|
155
|
+
| `--authName <name>` | API key parameter name | `apikey` |
|
|
156
|
+
| `--proxyPort <port>` | Port nginx listens on | `80` |
|
|
157
|
+
| `--customTemplate <path>` | Custom nginx config template | — |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Usage — Module (programmatic)
|
|
162
|
+
|
|
163
|
+
### Setup
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
const slaWizard = require("sla-wizard");
|
|
167
|
+
const customBaseUrlPlugin = require("sla-wizard-plugin-custom-baseUrl");
|
|
168
|
+
|
|
169
|
+
slaWizard.use(customBaseUrlPlugin);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`slaWizard.use` registers the plugin and exposes its functions directly on the
|
|
173
|
+
`slaWizard` object, injecting the sla-wizard context automatically.
|
|
174
|
+
|
|
175
|
+
### `slaWizard.configNginxBaseUrl(options)` — full config
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
slaWizard.configNginxBaseUrl({
|
|
179
|
+
outDir: "./nginx-output",
|
|
180
|
+
oas: "./specs/oas.yaml",
|
|
181
|
+
sla: "./specs/slas", // file, directory, or URL
|
|
182
|
+
authLocation: "header", // optional, default: "header"
|
|
183
|
+
authName: "apikey", // optional, default: "apikey"
|
|
184
|
+
proxyPort: 80, // optional, default: 80
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `slaWizard.addToBaseUrlConfd(options)` — conf.d only
|
|
189
|
+
|
|
190
|
+
Generates (or updates) only the `conf.d/` files without creating or overwriting
|
|
191
|
+
`nginx.conf`. Ideal for incremental user management:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
slaWizard.addToBaseUrlConfd({
|
|
195
|
+
outDir: "./nginx-output",
|
|
196
|
+
oas: "./specs/oas.yaml",
|
|
197
|
+
sla: "./specs/slas/sla_newuser.yaml",
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Using individual exports directly
|
|
202
|
+
|
|
203
|
+
The plugin also exports lower-level functions that work **without sla-wizard
|
|
204
|
+
context** — useful when integrating into custom pipelines:
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
const {
|
|
208
|
+
applyBaseUrlToConfig, // transform a raw config string
|
|
209
|
+
applyBaseUrlTransformations, // transform all .conf files in an output directory
|
|
210
|
+
} = require("sla-wizard-plugin-custom-baseUrl");
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### `applyBaseUrlTransformations(outDir, oasPath)`
|
|
214
|
+
|
|
215
|
+
Reads the OAS from `oasPath`, then for every endpoint that declares
|
|
216
|
+
`x-nginx-server-baseurl`, rewrites every matching `proxy_pass` directive inside
|
|
217
|
+
`<outDir>/nginx.conf` and `<outDir>/conf.d/*.conf` — replacing the default
|
|
218
|
+
server URL with the endpoint-specific custom URL.
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
const { applyBaseUrlTransformations } = require("sla-wizard-plugin-custom-baseUrl");
|
|
222
|
+
|
|
223
|
+
// After generating nginx config into outDir with any other tool...
|
|
224
|
+
applyBaseUrlTransformations("./nginx-output", "./specs/oas.yaml");
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### `applyBaseUrlToConfig(configContent, baseUrlMap, defaultUrl)`
|
|
228
|
+
|
|
229
|
+
Lower-level string transformation. Takes the raw config text, a map of
|
|
230
|
+
`sanitizedEndpoint → customUrl`, and the default URL to replace, and returns
|
|
231
|
+
the modified config string.
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
const { applyBaseUrlToConfig } = require("sla-wizard-plugin-custom-baseUrl");
|
|
235
|
+
|
|
236
|
+
const modified = applyBaseUrlToConfig(nginxConfString, {
|
|
237
|
+
modelschatgptv1chatcompletions: "http://localhost:8001",
|
|
238
|
+
modelsclaudev1chatcompletions: "http://localhost:8002",
|
|
239
|
+
}, "http://localhost:8000");
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Combining with `x-nginx-strip`
|
|
245
|
+
|
|
246
|
+
The plugin is built on top of `sla-wizard-plugin-nginx-strip`, so **both
|
|
247
|
+
extensions work together on the same OAS path**. When you annotate an endpoint
|
|
248
|
+
with both, nginx will:
|
|
249
|
+
|
|
250
|
+
1. Receive the full public path (e.g. `/models/chatgpt/v1/chat/completions`)
|
|
251
|
+
2. Forward the stripped backend path (e.g. `/v1/chat/completions`) — from `x-nginx-strip`
|
|
252
|
+
3. Send the request to the custom backend URL (e.g. `http://localhost:8001`) — from `x-nginx-server-baseurl`
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
paths:
|
|
256
|
+
/models/chatgpt/v1/chat/completions:
|
|
257
|
+
x-nginx-strip: "/models/chatgpt" # strip this prefix before forwarding
|
|
258
|
+
x-nginx-server-baseurl: http://localhost:8001 # forward to this backend
|
|
259
|
+
|
|
260
|
+
/models/claude/v1/chat/completions:
|
|
261
|
+
x-nginx-strip: "/models/claude"
|
|
262
|
+
x-nginx-server-baseurl: http://localhost:8002
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
When using `x-nginx-strip`, your SLAs can reference the backend (stripped) path
|
|
266
|
+
and the plugin expands them automatically:
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
plan:
|
|
270
|
+
rates:
|
|
271
|
+
/v1/chat/completions: # stripped path — expanded to all matching OAS paths
|
|
272
|
+
post:
|
|
273
|
+
requests:
|
|
274
|
+
- max: 10
|
|
275
|
+
period: minute
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Complete example
|
|
281
|
+
|
|
282
|
+
### OAS (`oas.yaml`)
|
|
283
|
+
|
|
284
|
+
```yaml
|
|
285
|
+
openapi: 3.0.3
|
|
286
|
+
info:
|
|
287
|
+
title: LLM Gateway API
|
|
288
|
+
version: 1.0.0
|
|
289
|
+
servers:
|
|
290
|
+
- url: http://localhost:8000 # default backend
|
|
291
|
+
|
|
292
|
+
paths:
|
|
293
|
+
/models/qwen/v1/chat/completions:
|
|
294
|
+
post:
|
|
295
|
+
summary: Qwen completions
|
|
296
|
+
operationId: postQwenCompletion
|
|
297
|
+
responses:
|
|
298
|
+
"200":
|
|
299
|
+
description: OK
|
|
300
|
+
|
|
301
|
+
/models/chatgpt/v1/chat/completions:
|
|
302
|
+
x-nginx-server-baseurl: http://localhost:8001
|
|
303
|
+
post:
|
|
304
|
+
summary: ChatGPT completions
|
|
305
|
+
operationId: postChatGPTCompletion
|
|
306
|
+
responses:
|
|
307
|
+
"200":
|
|
308
|
+
description: OK
|
|
309
|
+
|
|
310
|
+
/models/claude/v1/chat/completions:
|
|
311
|
+
x-nginx-server-baseurl: http://localhost:8002
|
|
312
|
+
post:
|
|
313
|
+
summary: Claude completions
|
|
314
|
+
operationId: postClaudeCompletion
|
|
315
|
+
responses:
|
|
316
|
+
"200":
|
|
317
|
+
description: OK
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### SLA (`sla.yaml`)
|
|
321
|
+
|
|
322
|
+
```yaml
|
|
323
|
+
sla4oas: 1.0.0
|
|
324
|
+
context:
|
|
325
|
+
id: sla-alice
|
|
326
|
+
type: agreement
|
|
327
|
+
api:
|
|
328
|
+
$ref: ./oas.yaml
|
|
329
|
+
apikeys:
|
|
330
|
+
- my-secret-key-abc123
|
|
331
|
+
plan:
|
|
332
|
+
name: standard
|
|
333
|
+
rates:
|
|
334
|
+
/models/chatgpt/v1/chat/completions:
|
|
335
|
+
post:
|
|
336
|
+
requests:
|
|
337
|
+
- max: 10
|
|
338
|
+
period: minute
|
|
339
|
+
/models/claude/v1/chat/completions:
|
|
340
|
+
post:
|
|
341
|
+
requests:
|
|
342
|
+
- max: 10
|
|
343
|
+
period: minute
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Generate config (programmatic)
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
const slaWizard = require("sla-wizard");
|
|
350
|
+
const customBaseUrlPlugin = require("sla-wizard-plugin-custom-baseUrl");
|
|
351
|
+
|
|
352
|
+
slaWizard.use(customBaseUrlPlugin);
|
|
353
|
+
|
|
354
|
+
slaWizard.configNginxBaseUrl({
|
|
355
|
+
outDir: "./nginx-output",
|
|
356
|
+
oas: "./oas.yaml",
|
|
357
|
+
sla: "./sla.yaml",
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Generated `conf.d/sla-alice_standard.conf` (excerpt)
|
|
362
|
+
|
|
363
|
+
```nginx
|
|
364
|
+
# Rate limiting zones
|
|
365
|
+
limit_req_zone $http_apikey zone=sla-alice_standard_modelschatgptv1chatcompletions_POST:10m rate=10r/m;
|
|
366
|
+
limit_req_zone $http_apikey zone=sla-alice_standard_modelsclaudev1chatcompletions_POST:10m rate=10r/m;
|
|
367
|
+
|
|
368
|
+
# Endpoint locations
|
|
369
|
+
location /sla-alice_standard_modelschatgptv1chatcompletions_POST {
|
|
370
|
+
rewrite /sla-alice_standard_modelschatgptv1chatcompletions_POST $uri_original break;
|
|
371
|
+
proxy_pass http://localhost:8001; ← chatgpt backend
|
|
372
|
+
limit_req zone=sla-alice_standard_modelschatgptv1chatcompletions_POST burst=9 nodelay;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
location /sla-alice_standard_modelsclaudev1chatcompletions_POST {
|
|
376
|
+
rewrite /sla-alice_standard_modelsclaudev1chatcompletions_POST $uri_original break;
|
|
377
|
+
proxy_pass http://localhost:8002; ← claude backend
|
|
378
|
+
limit_req zone=sla-alice_standard_modelsclaudev1chatcompletions_POST burst=9 nodelay;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The qwen endpoint has no `x-nginx-server-baseurl`, so its location block (in
|
|
383
|
+
`nginx.conf`) keeps `proxy_pass http://localhost:8000`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Running the tests
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npm test
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The test suite covers:
|
|
394
|
+
|
|
395
|
+
- **Unit** — `applyBaseUrlToConfig`: replacement, non-replacement, edge cases (empty input, no location blocks, block without `proxy_pass`, indentation preservation, partial-URL guard)
|
|
396
|
+
- **Unit** — `applyBaseUrlTransformations`: file I/O, no-op when no extension present, graceful handling of missing files/directories
|
|
397
|
+
- **Integration** — `configNginxBaseUrl` and `addToBaseUrlConfd`: correct output structure, proxy_pass values per endpoint, both strip and baseurl transforms coexisting, idempotency, single-file SLA input
|
|
398
|
+
- **CLI** — both commands, `--help` output, missing required argument, single-file SLA input, stdout success messages
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## License
|
|
403
|
+
|
|
404
|
+
Apache License 2.0 — same as SLA Wizard.
|
package/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const { configNginxBaseUrl, addToBaseUrlConfd } = require("./src/commands");
|
|
2
|
+
const {
|
|
3
|
+
applyBaseUrlToConfig,
|
|
4
|
+
applyBaseUrlTransformations,
|
|
5
|
+
} = require("./src/nginx-transform");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Plugin that generates nginx configuration with per-endpoint proxy_pass
|
|
9
|
+
* overrides driven by the x-nginx-server-baseurl OAS vendor extension.
|
|
10
|
+
*
|
|
11
|
+
* When a path in the OAS declares:
|
|
12
|
+
* x-nginx-server-baseurl: http://backend-host:port
|
|
13
|
+
*
|
|
14
|
+
* the generated nginx location blocks for that endpoint will use the specified
|
|
15
|
+
* URL in their proxy_pass directive instead of the global servers[0].url.
|
|
16
|
+
* Paths without the extension continue to use the global default.
|
|
17
|
+
*
|
|
18
|
+
* This plugin also inherits x-nginx-strip support (via sla-wizard-plugin-nginx-strip),
|
|
19
|
+
* so it works with OAS files that combine both extensions and SLAs that
|
|
20
|
+
* reference the stripped (backend) paths in their rate definitions.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} program - Commander program instance
|
|
23
|
+
* @param {Object} ctx - Context with utils and generate functions
|
|
24
|
+
*/
|
|
25
|
+
function apply(program, ctx) {
|
|
26
|
+
program
|
|
27
|
+
.command("config-nginx-baseurl")
|
|
28
|
+
.description(
|
|
29
|
+
"Generate nginx configuration with x-nginx-server-baseurl per-endpoint proxy_pass support",
|
|
30
|
+
)
|
|
31
|
+
.requiredOption(
|
|
32
|
+
"-o, --outDir <outputDirectory>",
|
|
33
|
+
"Output directory for nginx.conf and conf.d/",
|
|
34
|
+
)
|
|
35
|
+
.option(
|
|
36
|
+
"--sla <slaPath>",
|
|
37
|
+
"One of: 1) single SLA, 2) folder of SLAs, 3) URL returning an array of SLA objects",
|
|
38
|
+
"./specs/sla.yaml",
|
|
39
|
+
)
|
|
40
|
+
.option("--oas <pathToOAS>", "Path to an OAS v3 file.", "./specs/oas.yaml")
|
|
41
|
+
.option(
|
|
42
|
+
"--customTemplate <customTemplate>",
|
|
43
|
+
"Custom proxy configuration template.",
|
|
44
|
+
)
|
|
45
|
+
.option(
|
|
46
|
+
"--authLocation <authLocation>",
|
|
47
|
+
"Where to look for the authentication parameter. Must be one of: header, query, url.",
|
|
48
|
+
"header",
|
|
49
|
+
)
|
|
50
|
+
.option(
|
|
51
|
+
"--authName <authName>",
|
|
52
|
+
'Name of the authentication parameter, such as "token" or "apikey".',
|
|
53
|
+
"apikey",
|
|
54
|
+
)
|
|
55
|
+
.option("--proxyPort <proxyPort>", "Port on which the proxy is running", 80)
|
|
56
|
+
.action(function (options) {
|
|
57
|
+
configNginxBaseUrl(options, ctx);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command("add-to-baseurl-confd")
|
|
62
|
+
.description(
|
|
63
|
+
"Generate conf.d files with x-nginx-server-baseurl proxy_pass overrides (no nginx.conf)",
|
|
64
|
+
)
|
|
65
|
+
.requiredOption(
|
|
66
|
+
"-o, --outDir <outputDirectory>",
|
|
67
|
+
"Output directory for conf.d/",
|
|
68
|
+
)
|
|
69
|
+
.option(
|
|
70
|
+
"--sla <slaPath>",
|
|
71
|
+
"One of: 1) single SLA, 2) folder of SLAs, 3) URL returning an array of SLA objects",
|
|
72
|
+
"./specs/sla.yaml",
|
|
73
|
+
)
|
|
74
|
+
.option("--oas <pathToOAS>", "Path to an OAS v3 file.", "./specs/oas.yaml")
|
|
75
|
+
.option(
|
|
76
|
+
"--customTemplate <customTemplate>",
|
|
77
|
+
"Custom proxy configuration template.",
|
|
78
|
+
)
|
|
79
|
+
.option(
|
|
80
|
+
"--authLocation <authLocation>",
|
|
81
|
+
"Where to look for the authentication parameter. Must be one of: header, query, url.",
|
|
82
|
+
"header",
|
|
83
|
+
)
|
|
84
|
+
.option(
|
|
85
|
+
"--authName <authName>",
|
|
86
|
+
'Name of the authentication parameter, such as "token" or "apikey".',
|
|
87
|
+
"apikey",
|
|
88
|
+
)
|
|
89
|
+
.option("--proxyPort <proxyPort>", "Port on which the proxy is running", 80)
|
|
90
|
+
.action(function (options) {
|
|
91
|
+
addToBaseUrlConfd(options, ctx);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
apply,
|
|
97
|
+
configNginxBaseUrl,
|
|
98
|
+
addToBaseUrlConfd,
|
|
99
|
+
applyBaseUrlToConfig,
|
|
100
|
+
applyBaseUrlTransformations,
|
|
101
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sla-wizard-plugin-custom-baseurl",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Plugin that overrides the nginx proxy_pass target per endpoint using the x-nginx-server-baseurl OAS extension",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "mocha ./tests/tests.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"js-yaml": "^4.1.0",
|
|
11
|
+
"sla-wizard-plugin-nginx-strip": "^1.0.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"chai": "^4.3.6",
|
|
15
|
+
"mocha": "^10.0.0",
|
|
16
|
+
"sla-wizard": "^1.1.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const nginxStrip = require("sla-wizard-plugin-nginx-strip");
|
|
2
|
+
const { applyBaseUrlTransformations } = require("./nginx-transform");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates full nginx config (nginx.conf + conf.d/) with both x-nginx-strip
|
|
6
|
+
* path stripping and x-nginx-server-baseurl per-endpoint proxy_pass overrides.
|
|
7
|
+
*
|
|
8
|
+
* Delegates to sla-wizard-plugin-nginx-strip for SLA path expansion and strip
|
|
9
|
+
* transforms, then applies the base URL transformation on top.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options - Command options (outDir, oas, sla, …)
|
|
12
|
+
* @param {Object} ctx - sla-wizard context
|
|
13
|
+
*/
|
|
14
|
+
function configNginxBaseUrl(options, ctx) {
|
|
15
|
+
nginxStrip.configNginxStrip(options, ctx);
|
|
16
|
+
applyBaseUrlTransformations(options.outDir, options.oas || "./specs/oas.yaml");
|
|
17
|
+
console.log("✓ x-nginx-server-baseurl transformations applied");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generates only conf.d/ files with both strip and base URL transformations.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} options - Command options (outDir, oas, sla, …)
|
|
24
|
+
* @param {Object} ctx - sla-wizard context
|
|
25
|
+
*/
|
|
26
|
+
function addToBaseUrlConfd(options, ctx) {
|
|
27
|
+
nginxStrip.addToStripConfd(options, ctx);
|
|
28
|
+
applyBaseUrlTransformations(options.outDir, options.oas || "./specs/oas.yaml");
|
|
29
|
+
console.log("✓ x-nginx-server-baseurl transformations applied");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { configNginxBaseUrl, addToBaseUrlConfd };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const jsyaml = require("js-yaml");
|
|
4
|
+
const { sanitizeEndpoint } = require("./sanitize");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Replaces proxy_pass directives in location blocks that correspond to
|
|
8
|
+
* endpoints declaring x-nginx-server-baseurl in the OAS.
|
|
9
|
+
*
|
|
10
|
+
* Scans the config line by line:
|
|
11
|
+
* - On a `location` line, checks whether any sanitized endpoint key appears
|
|
12
|
+
* in the location name. If so, marks that endpoint as "active".
|
|
13
|
+
* - On a `proxy_pass` line inside an active location, replaces the default
|
|
14
|
+
* server URL with the endpoint-specific custom URL.
|
|
15
|
+
* - On a closing `}` line, resets the active endpoint tracker.
|
|
16
|
+
*
|
|
17
|
+
* Location blocks generated by sla-wizard are flat (no nested braces), so the
|
|
18
|
+
* single-level `}` detection is reliable.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} configContent - nginx config file content
|
|
21
|
+
* @param {Object} baseUrlMap - Map of sanitizedEndpoint -> customBaseUrl
|
|
22
|
+
* @param {string} defaultUrl - The default proxy target (servers[0].url)
|
|
23
|
+
* @returns {string} Modified config content
|
|
24
|
+
*/
|
|
25
|
+
function applyBaseUrlToConfig(configContent, baseUrlMap, defaultUrl) {
|
|
26
|
+
if (Object.keys(baseUrlMap).length === 0) return configContent;
|
|
27
|
+
|
|
28
|
+
const lines = configContent.split("\n");
|
|
29
|
+
let currentSanitized = null;
|
|
30
|
+
const result = [];
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
|
|
35
|
+
// Detect start of a location block
|
|
36
|
+
if (trimmed.startsWith("location")) {
|
|
37
|
+
currentSanitized = null;
|
|
38
|
+
for (const sanitized of Object.keys(baseUrlMap)) {
|
|
39
|
+
if (trimmed.includes(sanitized)) {
|
|
40
|
+
currentSanitized = sanitized;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Replace proxy_pass inside a matched location block
|
|
47
|
+
if (currentSanitized !== null && trimmed.startsWith("proxy_pass")) {
|
|
48
|
+
result.push(line.replace(defaultUrl, baseUrlMap[currentSanitized]));
|
|
49
|
+
} else {
|
|
50
|
+
result.push(line);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Detect end of the location block (flat structure — no nested braces)
|
|
54
|
+
if (trimmed === "}" && currentSanitized !== null) {
|
|
55
|
+
currentSanitized = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads the OAS file, finds endpoints with x-nginx-server-baseurl, and
|
|
64
|
+
* rewrites the generated nginx config files so that their proxy_pass
|
|
65
|
+
* directives use the endpoint-specific base URL instead of the global default.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} outDir - Output directory containing nginx.conf and conf.d/
|
|
68
|
+
* @param {string} oasPath - Path to the OAS file
|
|
69
|
+
*/
|
|
70
|
+
function applyBaseUrlTransformations(outDir, oasPath) {
|
|
71
|
+
const oasDoc = jsyaml.load(fs.readFileSync(oasPath, "utf8"));
|
|
72
|
+
const defaultUrl = oasDoc.servers[0].url;
|
|
73
|
+
|
|
74
|
+
// Build map: sanitizedEndpoint -> customBaseUrl
|
|
75
|
+
const baseUrlMap = {};
|
|
76
|
+
for (const endpoint in oasDoc.paths) {
|
|
77
|
+
const customUrl = oasDoc.paths[endpoint]["x-nginx-server-baseurl"];
|
|
78
|
+
if (customUrl) {
|
|
79
|
+
baseUrlMap[sanitizeEndpoint(endpoint)] = customUrl;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Object.keys(baseUrlMap).length === 0) return;
|
|
84
|
+
|
|
85
|
+
// Post-process nginx.conf
|
|
86
|
+
const nginxConfPath = path.join(outDir, "nginx.conf");
|
|
87
|
+
if (fs.existsSync(nginxConfPath)) {
|
|
88
|
+
fs.writeFileSync(
|
|
89
|
+
nginxConfPath,
|
|
90
|
+
applyBaseUrlToConfig(
|
|
91
|
+
fs.readFileSync(nginxConfPath, "utf8"),
|
|
92
|
+
baseUrlMap,
|
|
93
|
+
defaultUrl,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Post-process conf.d/*.conf
|
|
99
|
+
const confDDir = path.join(outDir, "conf.d");
|
|
100
|
+
if (fs.existsSync(confDDir)) {
|
|
101
|
+
fs.readdirSync(confDDir).forEach((file) => {
|
|
102
|
+
if (file.endsWith(".conf")) {
|
|
103
|
+
const filePath = path.join(confDDir, file);
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
filePath,
|
|
106
|
+
applyBaseUrlToConfig(
|
|
107
|
+
fs.readFileSync(filePath, "utf8"),
|
|
108
|
+
baseUrlMap,
|
|
109
|
+
defaultUrl,
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { applyBaseUrlToConfig, applyBaseUrlTransformations };
|
package/src/sanitize.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors sla-wizard's sanitizeEndpoint: keeps only [A-Za-z0-9-] and drops
|
|
3
|
+
* everything else (slashes, underscores, dots, …).
|
|
4
|
+
*/
|
|
5
|
+
function sanitizeEndpoint(input) {
|
|
6
|
+
return input.replace(/[^A-Za-z0-9-]/g, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { sanitizeEndpoint };
|