zuplo 7.7.10 → 7.7.12
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/docs/ai-gateway/integrations/claude-code.mdx +32 -16
- package/docs/ai-gateway/integrations/github-copilot.mdx +165 -0
- package/docs/dedicated/advanced/host-header-override.mdx +93 -0
- package/docs/policies/ai-gateway-configuration-executor-v2-inbound/doc.md +21 -89
- package/docs/policies/ai-gateway-metering-v2-inbound/doc.md +156 -0
- package/docs/policies/ai-gateway-metering-v2-inbound/intro.md +4 -0
- package/docs/policies/ai-gateway-metering-v2-inbound/schema.json +12 -6
- package/docs/programmable-api/zuplo-json.mdx +4 -1
- package/docs/self-hosted/install.md +71 -28
- package/docs/self-hosted/overview.md +4 -4
- package/docs/self-hosted/requirements.md +12 -5
- package/docs/self-hosted/troubleshooting.md +3 -2
- package/docs/self-hosted/upgrade.md +5 -5
- package/package.json +5 -5
- package/docs/self-hosted/verify.md +0 -279
|
@@ -70,9 +70,9 @@ settings file:
|
|
|
70
70
|
- **Global:** `~/.claude/settings.json`
|
|
71
71
|
- **Project-specific:** `.claude/settings.json` in your project directory
|
|
72
72
|
|
|
73
|
-
Each approach below shows the `env`
|
|
74
|
-
Code after saving it, and all your Claude Code LLM requests
|
|
75
|
-
Gateway.
|
|
73
|
+
Each approach below shows the `env` and `modelOverrides` blocks to add to that
|
|
74
|
+
file. Restart Claude Code after saving it, and all your Claude Code LLM requests
|
|
75
|
+
route through the AI Gateway.
|
|
76
76
|
|
|
77
77
|
:::note
|
|
78
78
|
|
|
@@ -87,7 +87,7 @@ With this approach the gateway calls Anthropic with the API key you saved on the
|
|
|
87
87
|
[provider](../managing-providers.mdx). Claude Code presents only the app's API
|
|
88
88
|
key, and Anthropic bills the account that owns the provider key.
|
|
89
89
|
|
|
90
|
-
Add the following
|
|
90
|
+
Add the following configuration to the settings file:
|
|
91
91
|
|
|
92
92
|
```json
|
|
93
93
|
{
|
|
@@ -99,7 +99,13 @@ Add the following environment configuration to the settings file:
|
|
|
99
99
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "anthropic/claude-opus-5",
|
|
100
100
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "anthropic/claude-sonnet-5",
|
|
101
101
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "anthropic/claude-haiku-4-5",
|
|
102
|
-
"ANTHROPIC_DEFAULT_FABLE_MODEL": "anthropic/claude-fable-5"
|
|
102
|
+
"ANTHROPIC_DEFAULT_FABLE_MODEL": "anthropic/claude-fable-5-1"
|
|
103
|
+
},
|
|
104
|
+
"modelOverrides": {
|
|
105
|
+
"claude-opus-5": "anthropic/claude-opus-5",
|
|
106
|
+
"claude-sonnet-5": "anthropic/claude-sonnet-5",
|
|
107
|
+
"claude-haiku-4-5-20251001": "anthropic/claude-haiku-4-5-20251001",
|
|
108
|
+
"claude-fable-5-1": "anthropic/claude-fable-5-1"
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
```
|
|
@@ -183,7 +189,7 @@ from Claude Code instead, as described below.
|
|
|
183
189
|
|
|
184
190
|
### Configure Claude Code
|
|
185
191
|
|
|
186
|
-
Add the following
|
|
192
|
+
Add the following configuration to the settings file. Don't set
|
|
187
193
|
`ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_API_KEY` here or in your shell: either one
|
|
188
194
|
replaces your `claude.ai` login, and the gateway would then forward that value
|
|
189
195
|
to Anthropic as the credential.
|
|
@@ -198,7 +204,13 @@ to Anthropic as the credential.
|
|
|
198
204
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "anthropic/claude-opus-5",
|
|
199
205
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "anthropic/claude-sonnet-5",
|
|
200
206
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "anthropic/claude-haiku-4-5",
|
|
201
|
-
"ANTHROPIC_DEFAULT_FABLE_MODEL": "anthropic/claude-fable-5"
|
|
207
|
+
"ANTHROPIC_DEFAULT_FABLE_MODEL": "anthropic/claude-fable-5-1"
|
|
208
|
+
},
|
|
209
|
+
"modelOverrides": {
|
|
210
|
+
"claude-opus-5": "anthropic/claude-opus-5",
|
|
211
|
+
"claude-sonnet-5": "anthropic/claude-sonnet-5",
|
|
212
|
+
"claude-haiku-4-5-20251001": "anthropic/claude-haiku-4-5-20251001",
|
|
213
|
+
"claude-fable-5-1": "anthropic/claude-fable-5-1"
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
216
|
```
|
|
@@ -246,23 +258,27 @@ it currently serves, and use those ids.
|
|
|
246
258
|
None of Claude Code's built-in model names carry a provider prefix, so each of
|
|
247
259
|
the model variables above must be set to a prefixed reference:
|
|
248
260
|
|
|
249
|
-
- `ANTHROPIC_MODEL` — the
|
|
250
|
-
|
|
261
|
+
- `ANTHROPIC_MODEL` — the startup model. Set it to a prefixed reference so the
|
|
262
|
+
gateway can route the first request.
|
|
251
263
|
- `ANTHROPIC_SMALL_FAST_MODEL` — the model for background tasks.
|
|
252
264
|
- `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`,
|
|
253
265
|
`ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_DEFAULT_FABLE_MODEL` — what the
|
|
254
266
|
`opus`/`sonnet`/`haiku`/`fable` aliases resolve to, so `claude --model opus`
|
|
255
267
|
and `/model sonnet` keep working. In these examples,
|
|
256
|
-
`ANTHROPIC_DEFAULT_FABLE_MODEL` maps `fable` to `anthropic/claude-fable-5`.
|
|
268
|
+
`ANTHROPIC_DEFAULT_FABLE_MODEL` maps `fable` to `anthropic/claude-fable-5-1`.
|
|
269
|
+
|
|
270
|
+
Claude Code's `/model` picker can select either an alias or an exact model ID.
|
|
271
|
+
`ANTHROPIC_DEFAULT_*_MODEL` maps aliases to gateway model names;
|
|
272
|
+
[`modelOverrides`](https://code.claude.com/docs/en/model-config#override-model-ids-per-version)
|
|
273
|
+
maps exact IDs. The examples configure both so either selection works through
|
|
274
|
+
the gateway.
|
|
257
275
|
|
|
258
276
|
:::caution{title='HTTP 400: model must use "providerName/model"'}
|
|
259
277
|
|
|
260
|
-
|
|
261
|
-
`
|
|
262
|
-
model
|
|
263
|
-
|
|
264
|
-
Code's built-in default model. Always set `ANTHROPIC_MODEL` to a prefixed model
|
|
265
|
-
reference.
|
|
278
|
+
This error means the selected model lacks its provider prefix. At startup, check
|
|
279
|
+
`ANTHROPIC_MODEL`. After choosing a picker entry, check its `modelOverrides`
|
|
280
|
+
entry, or type `/model fable` to use the configured alias. Restart Claude Code
|
|
281
|
+
after editing the settings file.
|
|
266
282
|
|
|
267
283
|
:::
|
|
268
284
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: GitHub Copilot
|
|
3
|
+
sidebar_label: GitHub Copilot
|
|
4
|
+
description:
|
|
5
|
+
Route GitHub Copilot in VS Code and the Copilot CLI through an AI Gateway app
|
|
6
|
+
so the gateway authenticates, meters, and routes every Copilot model request.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
[GitHub Copilot](https://github.com/features/copilot) can use your own model
|
|
10
|
+
endpoint instead of GitHub's hosted models — the
|
|
11
|
+
[Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-byok-models)
|
|
12
|
+
through environment variables, and VS Code through its
|
|
13
|
+
[Custom Endpoint provider](https://code.visualstudio.com/docs/agent-customization/language-models#_add-a-custom-endpoint-model).
|
|
14
|
+
Point that endpoint at an AI Gateway app, and every Copilot chat and agent
|
|
15
|
+
request is authenticated with the app's key, metered against the app's budgets,
|
|
16
|
+
and limited to the models you allow. Developers keep using Copilot exactly as
|
|
17
|
+
before; the app's page in the Zuplo Portal shows their requests, tokens, and
|
|
18
|
+
spend as they work.
|
|
19
|
+
|
|
20
|
+
## Before you start
|
|
21
|
+
|
|
22
|
+
<Stepper>
|
|
23
|
+
|
|
24
|
+
1. Create a [provider](../managing-providers.mdx) for the models Copilot will
|
|
25
|
+
use, such as OpenAI or Anthropic
|
|
26
|
+
|
|
27
|
+
2. [Create a team](../managing-teams.mdx) for the developers using Copilot
|
|
28
|
+
|
|
29
|
+
3. Create an [app](../managing-apps.mdx) for Copilot and assign it to the team
|
|
30
|
+
|
|
31
|
+
4. Copy the **API URL** and **API Key** shown at the top of the app page
|
|
32
|
+
|
|
33
|
+
</Stepper>
|
|
34
|
+
|
|
35
|
+
The gateway names models as `providerName/model`, where `providerName` is the
|
|
36
|
+
name you gave the provider. The examples below use `openai` and `anthropic`;
|
|
37
|
+
substitute your own provider names.
|
|
38
|
+
|
|
39
|
+
## Copilot CLI
|
|
40
|
+
|
|
41
|
+
The CLI reads its provider from four environment variables. Set the base URL to
|
|
42
|
+
the app's API URL plus `/v1`, and name a model that supports tool calls, such as
|
|
43
|
+
GPT-4.1:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export COPILOT_PROVIDER_BASE_URL="https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e/v1"
|
|
47
|
+
export COPILOT_PROVIDER_API_KEY="<your-ai-gateway-app-api-key>"
|
|
48
|
+
export COPILOT_PROVIDER_TYPE="openai"
|
|
49
|
+
export COPILOT_MODEL="openai/gpt-4.1-mini"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Start `copilot` from the same shell. Every request in the session now passes
|
|
53
|
+
through the gateway.
|
|
54
|
+
|
|
55
|
+
For a Claude model, use the `anthropic` provider type with the app's API URL
|
|
56
|
+
without `/v1` — the CLI appends `/v1/messages` itself — and pass the key as a
|
|
57
|
+
bearer token, which is the header the gateway reads:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export COPILOT_PROVIDER_BASE_URL="https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e"
|
|
61
|
+
export COPILOT_PROVIDER_BEARER_TOKEN="<your-ai-gateway-app-api-key>"
|
|
62
|
+
export COPILOT_PROVIDER_TYPE="anthropic"
|
|
63
|
+
export COPILOT_MODEL="anthropic/claude-sonnet-4-6"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The CLI speaks Chat Completions and the Messages API and sets a `temperature` on
|
|
67
|
+
every request. GPT-4.1, GPT-4o, Claude Sonnet 4.6, and Claude Haiku 4.5 all
|
|
68
|
+
accept that; OpenAI's GPT-5.x reasoning models and Claude Sonnet 5 don't, so use
|
|
69
|
+
them from VS Code instead.
|
|
70
|
+
|
|
71
|
+
## VS Code
|
|
72
|
+
|
|
73
|
+
Add the gateway as a Custom Endpoint provider, then describe your models in the
|
|
74
|
+
configuration file VS Code opens.
|
|
75
|
+
|
|
76
|
+
<Stepper>
|
|
77
|
+
|
|
78
|
+
1. Run **Chat: Manage Language Models** from the Command Palette
|
|
79
|
+
|
|
80
|
+
2. Select **Add Models**, then **Custom Endpoint**
|
|
81
|
+
|
|
82
|
+
3. Enter a name for the group, such as `Zuplo AI Gateway`, and paste the app's
|
|
83
|
+
API key
|
|
84
|
+
|
|
85
|
+
4. Select **Chat Completions** as the API type
|
|
86
|
+
|
|
87
|
+
5. Replace the `models` array in the `chatLanguageModels.json` file that opens
|
|
88
|
+
with the models you want in the picker
|
|
89
|
+
|
|
90
|
+
</Stepper>
|
|
91
|
+
|
|
92
|
+
This configuration offers a GPT model and a Claude model from one app:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
"name": "Zuplo AI Gateway",
|
|
98
|
+
"vendor": "customendpoint",
|
|
99
|
+
"apiKey": "${input:chat.lm.secret.321f64a2}",
|
|
100
|
+
"models": [
|
|
101
|
+
{
|
|
102
|
+
"id": "openai/gpt-5.6",
|
|
103
|
+
"name": "GPT-5.6 (Zuplo)",
|
|
104
|
+
"apiType": "responses",
|
|
105
|
+
"url": "https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e/v1/responses",
|
|
106
|
+
"toolCalling": true,
|
|
107
|
+
"vision": true,
|
|
108
|
+
"maxInputTokens": 128000,
|
|
109
|
+
"maxOutputTokens": 16000,
|
|
110
|
+
"requestHeaders": { "Authorization": "Bearer ${apiKey}" }
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "anthropic/claude-sonnet-5",
|
|
114
|
+
"name": "Claude Sonnet 5 (Zuplo)",
|
|
115
|
+
"apiType": "messages",
|
|
116
|
+
"url": "https://my-gateway-main-2e18f50.zuplo.app/config_fe0a04972d2848e0a94ae4b8bcd1497e/v1/messages",
|
|
117
|
+
"toolCalling": true,
|
|
118
|
+
"vision": true,
|
|
119
|
+
"maxInputTokens": 200000,
|
|
120
|
+
"maxOutputTokens": 64000,
|
|
121
|
+
"requestHeaders": { "Authorization": "Bearer ${apiKey}" }
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Three lines carry the integration:
|
|
129
|
+
|
|
130
|
+
- **`apiKey`** is a reference to the key you pasted, which VS Code keeps in its
|
|
131
|
+
secret storage and reads at request time. Leave it as written; to change the
|
|
132
|
+
key, use **Chat: Manage Language Models**.
|
|
133
|
+
- **`requestHeaders`** sends the key as `Authorization: Bearer <key>`, the
|
|
134
|
+
header the gateway's
|
|
135
|
+
[Authentication policy](../../policies/ai-gateway-auth-v2-inbound.mdx) reads.
|
|
136
|
+
VS Code's default header depends on the API type, so set it on every model.
|
|
137
|
+
- **`apiType`** and the `url` path select the API the model speaks: `responses`
|
|
138
|
+
for OpenAI's reasoning models such as GPT-5.x, `messages` for Claude through
|
|
139
|
+
the gateway's native Messages passthrough, and `chat-completions` with a
|
|
140
|
+
`/v1/chat/completions` URL for other chat models. Set it on each model so
|
|
141
|
+
models with different APIs share one entry in the picker.
|
|
142
|
+
|
|
143
|
+
Save the file and pick a model from the chat model picker.
|
|
144
|
+
|
|
145
|
+
## What routes through the gateway
|
|
146
|
+
|
|
147
|
+
Chat and agent requests, on both surfaces. Copilot's inline code completions
|
|
148
|
+
stay on GitHub's infrastructure. The app's
|
|
149
|
+
[Model Filtering](../../policies/ai-gateway-model-filtering-v2-inbound.mdx)
|
|
150
|
+
policy decides which models Copilot may use.
|
|
151
|
+
|
|
152
|
+
## Troubleshooting
|
|
153
|
+
|
|
154
|
+
Copilot reports gateway errors as a retry failure, such as
|
|
155
|
+
`502 token expired or invalid: 401`. The gateway's own response is in the
|
|
156
|
+
**GitHub Copilot Chat** channel of VS Code's Output panel, or in the CLI's error
|
|
157
|
+
text.
|
|
158
|
+
|
|
159
|
+
| The gateway responds | Cause | Fix |
|
|
160
|
+
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
|
|
161
|
+
| `401` `Header configured by options.authHeader is missing` | The key was sent as `x-api-key` | VS Code: add `requestHeaders`. CLI: use `COPILOT_PROVIDER_BEARER_TOKEN` |
|
|
162
|
+
| `401` `Invalid Authorization Scheme` | VS Code's `apiKey` was replaced with the key itself, so the key sent was empty | Re-enter the key with **Chat: Manage Language Models** |
|
|
163
|
+
| `404` | The `url` stops at the app ID | Append `/v1/responses`, `/v1/messages`, or `/v1/chat/completions` |
|
|
164
|
+
| `Function tools with reasoning_effort are not supported` | A GPT-5.x model on Chat Completions | VS Code: `"apiType": "responses"`. CLI: use a GPT-4.1 or Claude model |
|
|
165
|
+
| `'temperature' does not support 0` or `temperature is deprecated` | The CLI's temperature setting on a GPT-5.x or Claude Sonnet 5 model | Use GPT-4.1, GPT-4o, Claude Sonnet 4.6, or Claude Haiku 4.5 |
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Managed Dedicated: Host Header Override"
|
|
3
|
+
sidebar_label: Host Header Override
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
By default, the `Host` header your gateway sends to a backend is derived from
|
|
7
|
+
the URL it connects to. Host header override lets your code send a `Host` header
|
|
8
|
+
that differs from the hostname in the request URL, so the gateway can connect to
|
|
9
|
+
one address while the backend sees the hostname it expects.
|
|
10
|
+
|
|
11
|
+
This setting is only available on [Managed Dedicated](../overview.mdx) instances
|
|
12
|
+
and is off by default.
|
|
13
|
+
|
|
14
|
+
## When to use it
|
|
15
|
+
|
|
16
|
+
Reach for this when the address you connect to and the hostname your backend
|
|
17
|
+
expects aren't the same:
|
|
18
|
+
|
|
19
|
+
- **Virtual-hosted backends.** A single load balancer, ingress controller, or
|
|
20
|
+
web server hosts several sites and picks one based on the `Host` header. Your
|
|
21
|
+
gateway connects to the shared address and names the site it wants.
|
|
22
|
+
- **Private networking.** With
|
|
23
|
+
[private networking](../aws-private-networking.mdx), you reach a backend
|
|
24
|
+
through an internal endpoint or IP whose DNS name doesn't match the public
|
|
25
|
+
hostname the backend is configured for, such as `https://10.0.4.12` with a
|
|
26
|
+
`Host` of `api.acme.com`.
|
|
27
|
+
- **Testing a new backend before DNS moves.** Point the gateway at the new
|
|
28
|
+
origin's address while sending the production `Host` header, so you can
|
|
29
|
+
validate the migration without changing public DNS.
|
|
30
|
+
- **Legacy backends keyed on the hostname.** Applications that route, build
|
|
31
|
+
links, or select a tenant from the `Host` header keep working when the gateway
|
|
32
|
+
sits in front of them.
|
|
33
|
+
|
|
34
|
+
:::caution
|
|
35
|
+
|
|
36
|
+
An incorrect `Host` header can send traffic to the wrong site on a shared
|
|
37
|
+
backend, and some applications trust the header when building redirects and
|
|
38
|
+
links. Enable this setting only if you need it, and set the header to a value
|
|
39
|
+
your code controls rather than one copied from client input.
|
|
40
|
+
|
|
41
|
+
:::
|
|
42
|
+
|
|
43
|
+
## Enable the setting
|
|
44
|
+
|
|
45
|
+
Add `allowHostHeaderOverride` to the `zuplo.jsonc` file at the root of your
|
|
46
|
+
project and deploy:
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
{
|
|
50
|
+
"version": 1,
|
|
51
|
+
"projectType": "managed-dedicated",
|
|
52
|
+
"allowHostHeaderOverride": true,
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The setting applies to the whole project. Without it, a `Host` header you set on
|
|
57
|
+
an outbound request is ignored.
|
|
58
|
+
|
|
59
|
+
:::note
|
|
60
|
+
|
|
61
|
+
The `zuplo.jsonc` file isn't editable in the Zuplo Portal. Connect your project
|
|
62
|
+
to [source control](../source-control.mdx) and edit the file there or push a
|
|
63
|
+
local change with git. See
|
|
64
|
+
[Project Configuration](../../programmable-api/zuplo-json.mdx) for the other
|
|
65
|
+
settings this file supports.
|
|
66
|
+
|
|
67
|
+
:::
|
|
68
|
+
|
|
69
|
+
## Example
|
|
70
|
+
|
|
71
|
+
Once the setting is enabled, set the `Host` header on any outbound request. This
|
|
72
|
+
[custom handler](../../handlers/custom-handler.mdx) connects to an internal load
|
|
73
|
+
balancer and tells it which site to serve:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";
|
|
77
|
+
|
|
78
|
+
export default async function (request: ZuploRequest, context: ZuploContext) {
|
|
79
|
+
const url = new URL(request.url);
|
|
80
|
+
|
|
81
|
+
return fetch(`${environment.INTERNAL_ORIGIN}${url.pathname}${url.search}`, {
|
|
82
|
+
method: request.method,
|
|
83
|
+
body: request.body,
|
|
84
|
+
headers: {
|
|
85
|
+
...Object.fromEntries(request.headers),
|
|
86
|
+
Host: "api.acme.com",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
With `INTERNAL_ORIGIN` set to `https://10.0.4.12`, the gateway connects to
|
|
93
|
+
`10.0.4.12` and the backend receives a request for `api.acme.com`.
|
|
@@ -355,34 +355,23 @@ have both actions, the `warn` value must be less than the `block` value. The
|
|
|
355
355
|
policy does not treat entries whose `budgetBy` is `app` as expression budget
|
|
356
356
|
rules.
|
|
357
357
|
|
|
358
|
-
###
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
| Custom context data | `context.custom.account.id` |
|
|
373
|
-
| Route data | `context.route.path` |
|
|
374
|
-
| Incoming request data | `context.incomingRequestProperties.country` |
|
|
375
|
-
| Request or context identity | `request.user.sub`, `context.requestId` |
|
|
376
|
-
| A key requiring brackets | `request.user.data["team-id"]` |
|
|
377
|
-
|
|
378
|
-
The `expression` field contains the complete expression. Do not wrap it in an
|
|
379
|
-
interpolation marker such as `${...}`. Expressions use values from the request
|
|
380
|
-
and context, including values set by earlier policies. Place policies that set
|
|
381
|
-
these values before Semantic Cache. You can change an expression in the
|
|
358
|
+
### Expressions
|
|
359
|
+
|
|
360
|
+
An expression selects one value from the request or its context, such as
|
|
361
|
+
`request.headers.get("x-user-id")` or `request.user.data["team-id"]`. Every
|
|
362
|
+
distinct value gets its own budget.
|
|
363
|
+
|
|
364
|
+
The expression grammar, the selectable properties, what a value must be, and
|
|
365
|
+
what happens when an expression does not resolve are documented once, on the
|
|
366
|
+
[AI Gateway Metering](/docs/policies/ai-gateway-metering-v2-inbound) policy
|
|
367
|
+
page. The same grammar applies here.
|
|
368
|
+
|
|
369
|
+
Two things are specific to application configuration. A policy that sets a value
|
|
370
|
+
an expression reads must run before Semantic Cache, which can answer without
|
|
371
|
+
reaching the rest of the chain. And you can change an expression in the
|
|
382
372
|
application configuration without rebuilding or redeploying the gateway.
|
|
383
373
|
|
|
384
|
-
|
|
385
|
-
the double quotes required by a bracket segment:
|
|
374
|
+
When writing raw JSON, escape the double quotes a bracket segment needs:
|
|
386
375
|
|
|
387
376
|
```json
|
|
388
377
|
{
|
|
@@ -390,58 +379,14 @@ the double quotes required by a bracket segment:
|
|
|
390
379
|
}
|
|
391
380
|
```
|
|
392
381
|
|
|
393
|
-
After a JSON parser decodes
|
|
382
|
+
After a JSON parser decodes that value the expression is
|
|
394
383
|
`request.user.data["team-id"]`; the backslashes are not part of its identity.
|
|
395
|
-
Code that
|
|
396
|
-
let its JSON serializer
|
|
384
|
+
Code that builds application configuration should create an ordinary string and
|
|
385
|
+
let its JSON serializer add the transport escaping.
|
|
397
386
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
Header expressions are terminal. Header names are case-insensitive; use
|
|
404
|
-
lowercase in stored expressions. The parser accepts the canonical
|
|
405
|
-
`request.headers.get("content-type")` form and the equivalent
|
|
406
|
-
`request.headers.content-type` and `request.headers["content-type"]` forms. The
|
|
407
|
-
canonical form keeps header access visually distinct from an ordinary object
|
|
408
|
-
property.
|
|
409
|
-
|
|
410
|
-
A dot property starts with an ASCII letter, `_`, or `$`, followed by those
|
|
411
|
-
characters, ASCII digits, or hyphens. Canonical rendering uses quoted brackets
|
|
412
|
-
for keys containing `$` or `-`, or any other non-identifier character. For
|
|
413
|
-
example, `context.custom.team-id` is accepted and its canonical form is
|
|
414
|
-
`context.custom["team-id"]`. Single- and double-quoted bracket properties are
|
|
415
|
-
accepted. Whitespace immediately inside brackets is accepted but is not
|
|
416
|
-
canonical.
|
|
417
|
-
|
|
418
|
-
The selectable data model is:
|
|
419
|
-
|
|
420
|
-
| Root | Selectable properties |
|
|
421
|
-
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
422
|
-
| `request` | `url`, `method`, `headers`, `user.sub`, `user.data`, `query`, `searchParams`, `params`, and the scalar Fetch request metadata `bodyUsed`, `cache`, `credentials`, `destination`, `integrity`, `keepalive`, `mode`, `redirect`, `referrer`, and `referrerPolicy` |
|
|
423
|
-
| `context` | `contextId`, `requestId`, `custom`, `route`, and `incomingRequestProperties` |
|
|
424
|
-
|
|
425
|
-
Expressions cannot read the request body, call logging or policy methods, or
|
|
426
|
-
access properties outside this table.
|
|
427
|
-
|
|
428
|
-
Expressions have these limits:
|
|
429
|
-
|
|
430
|
-
- The expression is at most 1,024 UTF-8 bytes and contains well-formed Unicode.
|
|
431
|
-
- Quoted keys are non-empty and do not contain double quotes, backslashes,
|
|
432
|
-
control characters, U+2028, or U+2029. String escapes are not supported.
|
|
433
|
-
- Canonical expressions contain no syntax whitespace. Spaces inside a quoted key
|
|
434
|
-
are part of the key.
|
|
435
|
-
- Wildcards, recursive descent, filters, numeric bracket indexes, calls other
|
|
436
|
-
than the terminal header `.get("name")`, function arguments, and roots other
|
|
437
|
-
than `request` and `context` are not supported. For example,
|
|
438
|
-
`request.url.substring(5, 1)` is not a supported expression.
|
|
439
|
-
- Traversal reads own properties from plain objects and keys from declared Maps.
|
|
440
|
-
It does not traverse arrays, class instances, inherited properties, or the
|
|
441
|
-
property names `__proto__`, `constructor`, and `prototype`.
|
|
442
|
-
|
|
443
|
-
An authentication policy can populate `request.user`. A custom policy earlier in
|
|
444
|
-
the chain can derive a value and place it in a request header:
|
|
387
|
+
An expression selects one value and cannot combine several. To budget by a
|
|
388
|
+
composite value, compute it in a custom policy earlier in the chain and store it
|
|
389
|
+
somewhere an expression can read:
|
|
445
390
|
|
|
446
391
|
```ts
|
|
447
392
|
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
@@ -463,19 +408,6 @@ The corresponding expression is `request.headers.get("x-budget-customer")`.
|
|
|
463
408
|
Configuration code should create that ordinary string and pass the enclosing
|
|
464
409
|
object to `JSON.stringify`; the serializer adds the JSON transport escaping.
|
|
465
410
|
|
|
466
|
-
The expression chooses one value; it does not concatenate values, run
|
|
467
|
-
JavaScript, or evaluate conditions. Compute composite values in an earlier
|
|
468
|
-
policy and store the result in a supported request or context property.
|
|
469
|
-
|
|
470
|
-
The selected value must be a string or a safe integer. The runtime converts the
|
|
471
|
-
value to well-formed NFC Unicode and ignores it when it exceeds 256 UTF-8 bytes
|
|
472
|
-
or contains control characters, U+2028, or U+2029. A missing or invalid value
|
|
473
|
-
does not contribute to the rule for that request.
|
|
474
|
-
|
|
475
|
-
Use consistent spelling for each expression. Different spellings, such as single
|
|
476
|
-
quotes and double quotes, identify different budget rules even when they select
|
|
477
|
-
the same value.
|
|
478
|
-
|
|
479
411
|
Invalid budget rules and unsupported expressions are logged and skipped. Other
|
|
480
412
|
valid rules continue to apply.
|
|
481
413
|
|
|
@@ -68,6 +68,162 @@ value of its expression. An action of `"warn"` notifies without blocking. An
|
|
|
68
68
|
action of `"block"` activates the configured quota fallback or returns
|
|
69
69
|
`429 Too Many Requests` when usage reaches the value.
|
|
70
70
|
|
|
71
|
+
## Budget expressions
|
|
72
|
+
|
|
73
|
+
A rule with `"budgetBy": "expression"` gives every distinct value of its
|
|
74
|
+
expression its own budget. A daily cost budget of five dollars on
|
|
75
|
+
`request.headers.get("x-user-id")` gives each user five dollars a day, rather
|
|
76
|
+
than five dollars shared across all users.
|
|
77
|
+
|
|
78
|
+
An expression selects one value from the request or its context. It is a
|
|
79
|
+
selector, not code: there are no comparisons, arithmetic, string concatenation,
|
|
80
|
+
or method calls other than the header accessor below.
|
|
81
|
+
|
|
82
|
+
### Supported expressions
|
|
83
|
+
|
|
84
|
+
| Expression | Example | Selects |
|
|
85
|
+
| ------------------------------------------ | ------------------------------------------- | --------------------------------------------------------------- |
|
|
86
|
+
| `request.headers.get("<name>")` | `request.headers.get("x-user-id")` | One request header. Header names are case-insensitive. |
|
|
87
|
+
| `request.query.<name>` | `request.query.tenant` | One query-string parameter. |
|
|
88
|
+
| `request.searchParams.<name>` | `request.searchParams.tenant` | One query-string parameter. |
|
|
89
|
+
| `request.params.<name>` | `request.params.customerId` | One path parameter from the matched route. |
|
|
90
|
+
| `request.user.sub` | `request.user.sub` | The authenticated consumer's subject, such as the API key name. |
|
|
91
|
+
| `request.user.data.<property>` | `request.user.data.organizationId` | A property of the consumer's metadata, including nested ones. |
|
|
92
|
+
| `request.<property>` | `request.method` | A scalar request property. |
|
|
93
|
+
| `context.custom.<property>` | `context.custom.tenantId` | A value your own policies or handlers put on `context.custom`. |
|
|
94
|
+
| `context.route.<property>` | `context.route.path` | A property of the matched route, such as `path` or `label`. |
|
|
95
|
+
| `context.incomingRequestProperties.<name>` | `context.incomingRequestProperties.country` | A connection property, such as `country`, `city`, or `asn`. |
|
|
96
|
+
|
|
97
|
+
The selectable scalar request properties are `bodyUsed`, `cache`, `credentials`,
|
|
98
|
+
`destination`, `integrity`, `keepalive`, `method`, `mode`, `redirect`,
|
|
99
|
+
`referrer`, `referrerPolicy`, and `url`.
|
|
100
|
+
|
|
101
|
+
Use quoted brackets for any key that is not a plain identifier:
|
|
102
|
+
`request.query["team-id"]`, `context.custom["tenant.id"]`.
|
|
103
|
+
|
|
104
|
+
`context.incomingRequestProperties` exposes connection data only: `country`,
|
|
105
|
+
`city`, `region`, `regionCode`, `continent`, `colo`, `latitude`, `longitude`,
|
|
106
|
+
`postalCode`, `metroCode`, `timezone`, `ip`, `asn`, `asOrganization`, and
|
|
107
|
+
`httpProtocol`. Client-certificate and mTLS verification fields are not
|
|
108
|
+
selectable. To budget per client certificate, run the mTLS Authentication policy
|
|
109
|
+
first and select the metadata it attaches, such as
|
|
110
|
+
`request.user.data.mtlsAuth.sha256Fingerprint`.
|
|
111
|
+
|
|
112
|
+
An expression is limited to 1024 bytes and 32 property segments.
|
|
113
|
+
|
|
114
|
+
### Examples
|
|
115
|
+
|
|
116
|
+
Give every end user their own daily spend budget, keyed by a header your
|
|
117
|
+
application sends:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"budgetBy": "expression",
|
|
122
|
+
"expression": "request.headers.get(\"x-user-id\")",
|
|
123
|
+
"meters": [
|
|
124
|
+
{ "meter": "cost", "period": "daily", "value": 5, "action": "block" }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Give every customer of yours their own monthly token budget, keyed by metadata
|
|
130
|
+
on the API key that made the request:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"budgetBy": "expression",
|
|
135
|
+
"expression": "request.user.data.organizationId",
|
|
136
|
+
"meters": [
|
|
137
|
+
{
|
|
138
|
+
"meter": "tokens",
|
|
139
|
+
"period": "monthly",
|
|
140
|
+
"value": 8000000,
|
|
141
|
+
"action": "warn"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"meter": "tokens",
|
|
145
|
+
"period": "monthly",
|
|
146
|
+
"value": 10000000,
|
|
147
|
+
"action": "block"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Cap requests per tenant per hour, keyed by a path parameter on a route such as
|
|
154
|
+
`/tenants/:tenantId/chat`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"budgetBy": "expression",
|
|
159
|
+
"expression": "request.params.tenantId",
|
|
160
|
+
"meters": [
|
|
161
|
+
{
|
|
162
|
+
"meter": "requests",
|
|
163
|
+
"period": "hourly",
|
|
164
|
+
"value": 1000,
|
|
165
|
+
"action": "block"
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Values
|
|
172
|
+
|
|
173
|
+
An expression must resolve to a string or a safe integer. The runtime converts
|
|
174
|
+
the value to well-formed NFC Unicode, and ignores it when it exceeds 256 UTF-8
|
|
175
|
+
bytes or contains control characters, U+2028, or U+2029. Values are
|
|
176
|
+
case-sensitive, so `Acme` and `acme` get separate budgets, but two strings that
|
|
177
|
+
normalize to the same NFC text share one budget.
|
|
178
|
+
|
|
179
|
+
When an expression resolves to nothing, or to something else such as an object,
|
|
180
|
+
an array, or a fractional number, that rule does not count or block the request.
|
|
181
|
+
Other rules still apply. Pair a rule on a value you must enforce with a policy
|
|
182
|
+
that rejects requests missing it, such as API key authentication or request
|
|
183
|
+
validation.
|
|
184
|
+
|
|
185
|
+
### Not supported
|
|
186
|
+
|
|
187
|
+
| Not supported | Use instead |
|
|
188
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------- |
|
|
189
|
+
| Request body values, such as `request.body.userId` | A header, a query parameter, a path parameter, or API key metadata. |
|
|
190
|
+
| Whole objects, such as `request.headers` | One value, such as `request.headers.get("x-user-id")`. |
|
|
191
|
+
| Comparisons, `&&`, ternaries, and literals | One selector per rule. An application can have up to five rules. |
|
|
192
|
+
| String methods, such as `.toLowerCase()` | The value as the client sends it. |
|
|
193
|
+
| Wildcards and multiple keys in one expression | One rule per value you want to budget. |
|
|
194
|
+
| Arrays, class instances, and inherited properties | A plain value the gateway or your own policy sets. |
|
|
195
|
+
| Per-request identifiers, such as `context.requestId` | A value shared by many requests, such as a user or tenant. |
|
|
196
|
+
|
|
197
|
+
An unsupported expression does not fail the request. The rule is skipped, and
|
|
198
|
+
the request is not budgeted by it.
|
|
199
|
+
|
|
200
|
+
### Keep the spelling stable
|
|
201
|
+
|
|
202
|
+
Each expression is stored exactly as you write it, and its budgets are tracked
|
|
203
|
+
under that exact text. `request.headers.get("x-user-id")` and
|
|
204
|
+
`request.headers["x-user-id"]` read the same header but keep separate budgets,
|
|
205
|
+
and editing a rule's expression starts its budgets over. Single quotes and
|
|
206
|
+
double quotes likewise identify different rules. Use the spellings in the table
|
|
207
|
+
above.
|
|
208
|
+
|
|
209
|
+
Several forms parse, so prefer the canonical one. A header is written
|
|
210
|
+
`request.headers.get("content-type")`, with a lowercase name; the equivalent
|
|
211
|
+
`request.headers.content-type` and `request.headers["content-type"]` are
|
|
212
|
+
accepted but keep separate budgets. A dot property starts with a letter, `_`, or
|
|
213
|
+
`$`; a key containing anything else takes quoted brackets, so
|
|
214
|
+
`context.custom.team-id` is canonically `context.custom["team-id"]`.
|
|
215
|
+
|
|
216
|
+
In raw JSON, escape the double quotes a bracket segment needs:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"expression": "request.user.data[\"team-id\"]"
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
After a JSON parser decodes that value the expression is
|
|
225
|
+
`request.user.data["team-id"]`; the backslashes are not part of its identity.
|
|
226
|
+
|
|
71
227
|
## Team limits
|
|
72
228
|
|
|
73
229
|
Application budgets apply only to that application. Parent team and gateway
|
|
@@ -3,6 +3,10 @@ requests and enforces spend, token, and request budgets. When a limit is
|
|
|
3
3
|
exceeded, it uses the model selection's quota fallback when one is configured or
|
|
4
4
|
returns `429 Too Many Requests`.
|
|
5
5
|
|
|
6
|
+
A budget rule covers the whole application, or gives every distinct value of an
|
|
7
|
+
expression its own budget, such as one budget per user with
|
|
8
|
+
`request.headers.get("x-user-id")`.
|
|
9
|
+
|
|
6
10
|
Place it after Model Filtering and Fallback Model, and before policies such as
|
|
7
11
|
Semantic Cache that may answer without calling a provider. Metering fails open
|
|
8
12
|
by default when its service is unavailable; set `throwOnFailure` to `true` to
|
|
@@ -77,17 +77,19 @@
|
|
|
77
77
|
"properties": {
|
|
78
78
|
"budgetBy": {
|
|
79
79
|
"type": "string",
|
|
80
|
-
"enum": ["app", "expression"]
|
|
80
|
+
"enum": ["app", "expression"],
|
|
81
|
+
"description": "Budget the application as a whole (\"app\"), or give every distinct value of an expression its own budget (\"expression\")."
|
|
81
82
|
},
|
|
82
83
|
"expression": {
|
|
83
84
|
"type": "string",
|
|
84
85
|
"minLength": 1,
|
|
85
|
-
"description": "Required when budgetBy is \"expression\"; forbidden for \"app\". Each distinct value gets its own budget."
|
|
86
|
+
"description": "Required when budgetBy is \"expression\"; forbidden for \"app\". Each distinct value gets its own budget. Select one value from the request or its context: request.headers.get(\"x-user-id\"), request.query.tenant, request.params.customerId, request.user.sub, request.user.data.organizationId, request.method, request.url, context.custom.tenantId, context.route.path, or context.incomingRequestProperties.country. Comparisons, arithmetic, string methods, and request body values are not supported."
|
|
86
87
|
},
|
|
87
88
|
"meters": {
|
|
88
89
|
"type": "array",
|
|
89
90
|
"minItems": 1,
|
|
90
91
|
"maxItems": 24,
|
|
92
|
+
"description": "Thresholds this rule enforces. Add one entry per meter, period, and action you want.",
|
|
91
93
|
"items": {
|
|
92
94
|
"type": "object",
|
|
93
95
|
"additionalProperties": false,
|
|
@@ -95,19 +97,23 @@
|
|
|
95
97
|
"properties": {
|
|
96
98
|
"meter": {
|
|
97
99
|
"type": "string",
|
|
98
|
-
"enum": ["cost", "requests", "tokens"]
|
|
100
|
+
"enum": ["cost", "requests", "tokens"],
|
|
101
|
+
"description": "What to measure: US dollars spent (\"cost\"), requests made (\"requests\"), or tokens used (\"tokens\")."
|
|
99
102
|
},
|
|
100
103
|
"period": {
|
|
101
104
|
"type": "string",
|
|
102
|
-
"enum": ["hourly", "daily", "weekly", "monthly"]
|
|
105
|
+
"enum": ["hourly", "daily", "weekly", "monthly"],
|
|
106
|
+
"description": "How often usage resets."
|
|
103
107
|
},
|
|
104
108
|
"value": {
|
|
105
109
|
"type": "number",
|
|
106
|
-
"exclusiveMinimum": 0
|
|
110
|
+
"exclusiveMinimum": 0,
|
|
111
|
+
"description": "The threshold for this meter and period. Cost is in US dollars."
|
|
107
112
|
},
|
|
108
113
|
"action": {
|
|
109
114
|
"type": "string",
|
|
110
|
-
"enum": ["warn", "block"]
|
|
115
|
+
"enum": ["warn", "block"],
|
|
116
|
+
"description": "What happens at the threshold: \"warn\" notifies and lets the request through, \"block\" uses the configured quota fallback model or returns 429 Too Many Requests."
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
}
|
|
@@ -89,11 +89,14 @@ to `false`.
|
|
|
89
89
|
|
|
90
90
|
Allow the Host header to be overridden by the client. This configuration
|
|
91
91
|
defaults to `false`. This is only supported in managed dedicated environments.
|
|
92
|
-
Only enable this setting if you understand the implications.
|
|
92
|
+
Only enable this setting if you understand the implications. See
|
|
93
|
+
[Host Header Override](../dedicated/advanced/host-header-override.mdx) for when
|
|
94
|
+
to use it and an example.
|
|
93
95
|
|
|
94
96
|
```jsonc
|
|
95
97
|
{
|
|
96
98
|
"version": 1,
|
|
99
|
+
"projectType": "managed-dedicated",
|
|
97
100
|
"allowHostHeaderOverride": true,
|
|
98
101
|
}
|
|
99
102
|
```
|
|
@@ -12,24 +12,22 @@ certificates from cert-manager. Before you begin, complete the
|
|
|
12
12
|
Set these variables in the shell that you use for the installation:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
# From the Zuplo portal. See Requirements for instructions.
|
|
16
|
-
export ZUPLO_ACCOUNT_NAME='acme-corp'
|
|
17
15
|
|
|
18
16
|
# Provided by Zuplo during onboarding.
|
|
19
17
|
export ZUPLO_CHART_VERSION='<chart version from onboarding>'
|
|
20
18
|
export ZUPLO_REGISTRY_KEY='<base64 credential from onboarding>'
|
|
21
19
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
export
|
|
20
|
+
# Provided by you
|
|
21
|
+
|
|
22
|
+
# These are the credentials to your gateway image registry.
|
|
23
|
+
export BUILDER_REGISTRY_USER='your-example-registry-user'
|
|
24
|
+
export BUILDER_REGISTRY_PASSWORD='your-example-registry-user-pw'
|
|
27
25
|
```
|
|
28
26
|
|
|
29
27
|
## Authenticate to the Zuplo registry
|
|
30
28
|
|
|
31
|
-
The chart is an OCI artifact in Zuplo's registry. The same credential
|
|
32
|
-
chart and the component images.
|
|
29
|
+
The Zuplo Helm chart is an OCI artifact in Zuplo's registry. The same credential
|
|
30
|
+
pulls the chart and the component images.
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
33
|
printf '%s' "$ZUPLO_REGISTRY_KEY" |
|
|
@@ -52,7 +50,8 @@ account:
|
|
|
52
50
|
|
|
53
51
|
deployments:
|
|
54
52
|
# Parent domain for your APIs. Needs wildcard DNS:
|
|
55
|
-
# *.api.example.com -> your ingress address
|
|
53
|
+
# *.api.example.com -> your ingress address. For example, a preview
|
|
54
|
+
# domain deployment might look like your-preview-branch-abs0ef.api.example.com.
|
|
56
55
|
subdomain: api.example.com
|
|
57
56
|
|
|
58
57
|
managementApi:
|
|
@@ -68,12 +67,11 @@ builder:
|
|
|
68
67
|
secretName: builder-secret
|
|
69
68
|
|
|
70
69
|
cert-manager:
|
|
71
|
-
#
|
|
72
|
-
# is what keeps a wildcard certificate out of the picture.
|
|
70
|
+
# cert-manager is used for managing the lifecyle of each deployment certificate.
|
|
73
71
|
enabled: true
|
|
74
72
|
acme:
|
|
75
|
-
# Required when cert-manager.enabled is true.
|
|
76
|
-
#
|
|
73
|
+
# Required when cert-manager.enabled is true. Used to register the ACME
|
|
74
|
+
# account with the certificate authority.
|
|
77
75
|
email: platform@example.com
|
|
78
76
|
```
|
|
79
77
|
|
|
@@ -91,17 +89,25 @@ zuploImageRegistry:
|
|
|
91
89
|
password: ${ZUPLO_REGISTRY_KEY}
|
|
92
90
|
|
|
93
91
|
builder:
|
|
94
|
-
username:
|
|
95
|
-
password: ${
|
|
92
|
+
username: ${BUILDER_REGISTRY_USER}
|
|
93
|
+
password: ${BUILDER_REGISTRY_PASSWORD}
|
|
96
94
|
EOF
|
|
97
95
|
```
|
|
98
96
|
|
|
99
|
-
:::danger{title="
|
|
97
|
+
:::danger{title="Protect registry credentials"}
|
|
98
|
+
|
|
99
|
+
This file contains registry credentials. Add `zuplo-secrets.yaml` to
|
|
100
|
+
`.gitignore` before creating it, restrict access to the file, and delete the
|
|
101
|
+
local copy after installation. Keep the credentials in your organization's
|
|
102
|
+
secret manager for future upgrades.
|
|
103
|
+
|
|
104
|
+
For automated installations, generate the file from CI secrets or a secret
|
|
105
|
+
manager. If you store the file in Git, encrypt it with a tool such as SOPS and
|
|
106
|
+
decrypt it when running Helm.
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
Secrets
|
|
103
|
-
|
|
104
|
-
store. Add it to `.gitignore` with your kubeconfig.
|
|
108
|
+
Helm stores these credentials in its release history, and the chart creates
|
|
109
|
+
Kubernetes Secrets from them. Restrict access to these Secrets, including Helm's
|
|
110
|
+
release Secrets.
|
|
105
111
|
|
|
106
112
|
:::
|
|
107
113
|
|
|
@@ -139,7 +145,8 @@ namespaces:
|
|
|
139
145
|
|
|
140
146
|
Without `--version`, Helm selects the most recent chart available at
|
|
141
147
|
installation time. Record the pinned version with `zuplo-values.yaml` so that
|
|
142
|
-
you can review and reproduce upgrades.
|
|
148
|
+
you can review and reproduce upgrades. Your Zuplo solutions architect will tell
|
|
149
|
+
you which version to install.
|
|
143
150
|
|
|
144
151
|
:::
|
|
145
152
|
|
|
@@ -156,16 +163,52 @@ NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
|
|
|
156
163
|
zuplo-haproxy-ingress LoadBalancer 10.128.82.11 203.0.113.24 80:31670/TCP,443:31011/TCP 50s
|
|
157
164
|
```
|
|
158
165
|
|
|
159
|
-
Create two DNS A records pointing at
|
|
160
|
-
neither behind a TLS-terminating proxy:
|
|
166
|
+
Create two DNS A records pointing at EXTERNAL-IP, both resolving publicly:
|
|
161
167
|
|
|
162
168
|
| Type | Name | Value |
|
|
163
169
|
| ---- | ------------------------- | -------------- |
|
|
164
170
|
| `A` | `*.api.example.com` | `203.0.113.24` |
|
|
165
171
|
| `A` | `zuplo-admin.example.com` | `203.0.113.24` |
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
Certificate issuance requires the HTTP-01 challenge URL at
|
|
174
|
+
http://<hostname>/.well-known/acme-challenge/<token> to be reachable from inside
|
|
175
|
+
the cluster and the public internet. If you use a proxy such as Cloudflare,
|
|
176
|
+
configure it to forward these requests to the cluster without authentication or
|
|
177
|
+
changes to the challenge response. cert-manager checks the URL before asking the
|
|
178
|
+
certificate authority to validate the challenge.
|
|
170
179
|
|
|
171
|
-
|
|
180
|
+
## Verify your installation
|
|
181
|
+
|
|
182
|
+
Use the [Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor)
|
|
183
|
+
to check the installation. Doctor is a CLI that reads your installation through
|
|
184
|
+
your kubeconfig and probes its DNS, TLS, and HTTP endpoints. It checks the Helm
|
|
185
|
+
release, the `Configuration` resource, Deployment health, ingress reachability,
|
|
186
|
+
certificate issuance, DNS records, the management API, and the builder
|
|
187
|
+
configuration. It reads the installation and doesn't change it.
|
|
188
|
+
|
|
189
|
+
Download the archive for your machine from the
|
|
190
|
+
[releases page](https://github.com/zuplo/self-hosted-doctor/releases), verify
|
|
191
|
+
its checksum, extract it, and run the full suite against your current kubeconfig
|
|
192
|
+
context:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
./zuplo-self-hosted-doctor verify
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Doctor exits `0` when no check failed, `1` when a check failed, and `3` when it
|
|
199
|
+
couldn't run. Each warning and failure names a diagnostic step and links to the
|
|
200
|
+
matching entry in [Troubleshooting](./troubleshooting.md).
|
|
201
|
+
|
|
202
|
+
To also check authenticated access to the management API, set `ZUPLO_API_KEY` in
|
|
203
|
+
your shell before running `verify`. Without a key, Doctor checks only that
|
|
204
|
+
unauthenticated requests are rejected.
|
|
205
|
+
|
|
206
|
+
The repository's README covers selecting individual checks, comparing a local
|
|
207
|
+
values file, private certificate authorities, and JSON output.
|
|
208
|
+
|
|
209
|
+
:::note
|
|
210
|
+
|
|
211
|
+
Doctor doesn't deploy a project. To confirm the full build-and-serve path, run
|
|
212
|
+
`npx zuplo deploy` against your account after Doctor passes.
|
|
213
|
+
|
|
214
|
+
:::
|
|
@@ -184,10 +184,10 @@ images, and the credentials to pull them.
|
|
|
184
184
|
## Get started
|
|
185
185
|
|
|
186
186
|
1. Review the [requirements](./requirements.md) and run the preflight checks.
|
|
187
|
-
2. [Install](./install.md) the Helm chart
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
2. [Install](./install.md) the Helm chart, then verify the installation with the
|
|
188
|
+
[Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor) CLI.
|
|
189
|
+
3. [Upgrade](./upgrade.md) to later chart versions when needed.
|
|
190
|
+
4. Use [troubleshooting](./troubleshooting.md) to resolve installation and
|
|
191
191
|
deployment errors.
|
|
192
192
|
|
|
193
193
|
To discuss your specific requirements,
|
|
@@ -105,11 +105,18 @@ For HTTP-01 validation, the certificate authority must also reach the ingress on
|
|
|
105
105
|
port 80. If your cluster can't expose port 80 to the public internet, discuss an
|
|
106
106
|
alternative certificate configuration with your Zuplo solutions architect.
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
The cluster also needs outbound access to:
|
|
109
|
+
|
|
110
|
+
- Zuplo services for deployment configuration and management API authentication.
|
|
111
|
+
- Zuplo's private container registry and the public registries used by the
|
|
112
|
+
chart's bundled components.
|
|
113
|
+
- Your container registry to push and pull gateway images.
|
|
114
|
+
- Docker Hub and Debian package repositories to build gateway images.
|
|
115
|
+
- Your ACME certificate authority to issue and renew certificates.
|
|
116
|
+
|
|
117
|
+
The Helm chart installs the required components, but the cluster downloads their
|
|
118
|
+
container images and gateway build dependencies. If you restrict outbound
|
|
119
|
+
traffic, allow access to these dependencies.
|
|
113
120
|
|
|
114
121
|
:::note
|
|
115
122
|
|
|
@@ -202,8 +202,9 @@ to HTTPS.
|
|
|
202
202
|
|
|
203
203
|
This redirect doesn't affect certificate issuance. cert-manager creates a
|
|
204
204
|
separate challenge Ingress that continues to serve on port 80. To test the
|
|
205
|
-
ingress, use a hostname with no Ingress
|
|
206
|
-
[
|
|
205
|
+
ingress, use a hostname with no Ingress. The
|
|
206
|
+
[Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor) CLI does
|
|
207
|
+
this for you in its `ingress-reachable` check.
|
|
207
208
|
|
|
208
209
|
## Failed to deploy the environment but the cluster shows the build succeeding
|
|
209
210
|
|
|
@@ -41,13 +41,13 @@ installed with `helm plugin install https://github.com/databus23/helm-diff`.
|
|
|
41
41
|
|
|
42
42
|
## Verify the upgrade
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
Run the [Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor)
|
|
45
|
+
CLI again and confirm the release history records the new revision. The gateway
|
|
46
|
+
deployments already in the cluster keep serving through the upgrade; they are
|
|
47
|
+
rebuilt only when you deploy them.
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
|
-
|
|
50
|
-
-n zuplo-system --timeout=5m
|
|
50
|
+
./zuplo-self-hosted-doctor verify
|
|
51
51
|
helm history zuplo -n zuplo
|
|
52
52
|
```
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The official Zuplo CLI for local development and platform management",
|
|
6
6
|
"homepage": "https://zuplo.com/docs/cli/overview",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"zuplo": "zuplo.js"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zuplo/cli": "7.7.
|
|
36
|
-
"@zuplo/core": "7.7.
|
|
37
|
-
"@zuplo/runtime": "7.7.
|
|
38
|
-
"@zuplo/test": "7.7.
|
|
35
|
+
"@zuplo/cli": "7.7.12",
|
|
36
|
+
"@zuplo/core": "7.7.12",
|
|
37
|
+
"@zuplo/runtime": "7.7.12",
|
|
38
|
+
"@zuplo/test": "7.7.12"
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Verify Your Install
|
|
3
|
-
sidebar_label: Verify your install
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Run the six checks in order. Each check depends on the previous one, which helps
|
|
7
|
-
you identify the source of a failure. Checks one through five require only the
|
|
8
|
-
installation. Check six also requires a Zuplo API key and project.
|
|
9
|
-
|
|
10
|
-
## Check the deployments
|
|
11
|
-
|
|
12
|
-
Gate on readiness before testing anything over the network.
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
kubectl wait --for=condition=Available deployment --all \
|
|
16
|
-
-n zuplo-system --timeout=5m
|
|
17
|
-
kubectl wait --for=condition=Available deployment --all \
|
|
18
|
-
-n zuplo --timeout=5m
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
deployment.apps/control-plane condition met
|
|
23
|
-
deployment.apps/deployer condition met
|
|
24
|
-
deployment.apps/gateway condition met
|
|
25
|
-
deployment.apps/storage condition met
|
|
26
|
-
deployment.apps/acme-forwarder condition met
|
|
27
|
-
deployment.apps/custom-error-pages condition met
|
|
28
|
-
deployment.apps/zuplo-cert-manager condition met
|
|
29
|
-
deployment.apps/zuplo-cert-manager-cainjector condition met
|
|
30
|
-
deployment.apps/zuplo-cert-manager-webhook condition met
|
|
31
|
-
deployment.apps/zuplo-haproxy-ingress condition met
|
|
32
|
-
deployment.apps/zuplo-kube-prometheus-stac-operator condition met
|
|
33
|
-
deployment.apps/zuplo-kube-state-metrics condition met
|
|
34
|
-
deployment.apps/zuplo-prometheus-adapter condition met
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
The `control-plane`, `deployer`, `gateway`, `storage`, and `acme-forwarder`
|
|
38
|
-
deployments are part of the Zuplo management plane. The other deployments come
|
|
39
|
-
from subcharts.
|
|
40
|
-
|
|
41
|
-
## Check the ingress
|
|
42
|
-
|
|
43
|
-
Send a request for a hostname that has no Ingress. This checks that the load
|
|
44
|
-
balancer can reach HAProxy.
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
LB=$(kubectl get svc zuplo-haproxy-ingress -n zuplo \
|
|
48
|
-
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
|
49
|
-
|
|
50
|
-
curl -i "http://$LB/" -H 'Host: no-such-deployment.example.com'
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
HTTP/1.1 404 Not Found
|
|
55
|
-
content-type: text/html
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
"type": "https://httpproblems.com/http-status/404",
|
|
59
|
-
"title": "Not Found",
|
|
60
|
-
"status": 404,
|
|
61
|
-
"detail": "This Zuplo project does not exist"
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The expected result is `404 Not Found` from HAProxy's default backend. A timeout
|
|
66
|
-
or refused connection means that traffic isn't reaching the ingress controller.
|
|
67
|
-
|
|
68
|
-
Use a hostname without an Ingress for this check. Hosts with an Ingress redirect
|
|
69
|
-
port 80 to HTTPS and return a `302` instead.
|
|
70
|
-
|
|
71
|
-
:::note
|
|
72
|
-
|
|
73
|
-
Requests for hosts with an Ingress are redirected from port 80 to 443. The
|
|
74
|
-
separate challenge Ingress created by cert-manager continues to serve ACME
|
|
75
|
-
challenge paths on port 80.
|
|
76
|
-
|
|
77
|
-
:::
|
|
78
|
-
|
|
79
|
-
## Check the control plane
|
|
80
|
-
|
|
81
|
-
The control plane turns your values into a `Configuration` resource, then
|
|
82
|
-
creates the management API's Ingress from it.
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
kubectl get configuration default -n zuplo-system -o yaml
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
```yaml
|
|
89
|
-
spec:
|
|
90
|
-
account:
|
|
91
|
-
name: acme-corp
|
|
92
|
-
builder:
|
|
93
|
-
registry: us-docker.pkg.dev/acme-corp/zuplo-gateways
|
|
94
|
-
secretName: builder-secret
|
|
95
|
-
deployments:
|
|
96
|
-
subdomain: api.example.com
|
|
97
|
-
certificates:
|
|
98
|
-
certManager:
|
|
99
|
-
issuer: zuplo-cluster-issuer
|
|
100
|
-
managementApi:
|
|
101
|
-
hostname: zuplo-admin.example.com
|
|
102
|
-
ingressControllers:
|
|
103
|
-
- haproxy
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Confirm every value matches what you put in `zuplo-values.yaml`. A
|
|
107
|
-
single-cluster installation has no `workers` or `authorization` entry. Those
|
|
108
|
-
settings are for installations that distribute deployments across clusters.
|
|
109
|
-
|
|
110
|
-
Then confirm the reconciler acted on the rest:
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
kubectl get ingress -n zuplo-system
|
|
114
|
-
kubectl get clusterissuer
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
NAME CLASS HOSTS ADDRESS PORTS AGE
|
|
119
|
-
gateway-haproxy haproxy zuplo-admin.example.com 203.0.113.24 80, 443 62s
|
|
120
|
-
|
|
121
|
-
NAME READY STATUS AGE
|
|
122
|
-
zuplo-cluster-issuer True The ACME account was registered with the ACME server 116s
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
`READY: True` for `zuplo-cluster-issuer` means that the chart registered an
|
|
126
|
-
account with the certificate authority. It doesn't indicate whether a
|
|
127
|
-
certificate has been issued.
|
|
128
|
-
|
|
129
|
-
:::note
|
|
130
|
-
|
|
131
|
-
`Configuration` doesn't report a status. Check the Ingress and ClusterIssuer
|
|
132
|
-
resources that it produces instead.
|
|
133
|
-
|
|
134
|
-
:::
|
|
135
|
-
|
|
136
|
-
## Check certificate issuance
|
|
137
|
-
|
|
138
|
-
Check that the certificate authority issued a certificate for the management API
|
|
139
|
-
hostname:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
kubectl get certificate -A
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
NAMESPACE NAME READY SECRET AGE
|
|
147
|
-
zuplo-system gateway-cert-manager True gateway-cert-manager 4m
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
`READY: True` means that the certificate authority validated your hostname and
|
|
151
|
-
issued a certificate. If it remains `False` for more than a few minutes, see
|
|
152
|
-
[Troubleshooting](./troubleshooting.md).
|
|
153
|
-
|
|
154
|
-
cert-manager retries automatically after you fix DNS. You don't need to restart
|
|
155
|
-
it.
|
|
156
|
-
|
|
157
|
-
## Check the management API
|
|
158
|
-
|
|
159
|
-
Send an unauthenticated request to check DNS, the load balancer, HAProxy, the
|
|
160
|
-
certificate, and authentication:
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
curl -i https://zuplo-admin.example.com/v1/deployments
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
```
|
|
167
|
-
HTTP/2 401
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
"type": "https://httpproblems.com/http-status/401",
|
|
171
|
-
"title": "Unauthorized",
|
|
172
|
-
"status": 401,
|
|
173
|
-
"detail": "No Authorization Header",
|
|
174
|
-
...
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
The expected result is `401 Unauthorized`. It confirms that `curl` completed a
|
|
179
|
-
TLS handshake with a trusted certificate and that the gateway requires an API
|
|
180
|
-
key. A TLS error indicates a certificate problem. A timeout indicates a DNS or
|
|
181
|
-
load balancer problem.
|
|
182
|
-
|
|
183
|
-
Confirm the certificate is the real one rather than HAProxy's self-signed
|
|
184
|
-
fallback:
|
|
185
|
-
|
|
186
|
-
```bash
|
|
187
|
-
echo | openssl s_client -connect zuplo-admin.example.com:443 \
|
|
188
|
-
-servername zuplo-admin.example.com 2>/dev/null |
|
|
189
|
-
openssl x509 -noout -subject -issuer
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
subject=CN=zuplo-admin.example.com
|
|
194
|
-
issuer=C=US, O=Let's Encrypt, CN=YR2
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
An issuer of `CN=kubernetes-ingress-ca` means HAProxy is serving its built-in
|
|
198
|
-
fallback certificate because no issued certificate is available. Repeat the
|
|
199
|
-
certificate issuance check.
|
|
200
|
-
|
|
201
|
-
## Deploy and call a project
|
|
202
|
-
|
|
203
|
-
The final check builds a gateway image in your cluster, stores it in your
|
|
204
|
-
registry, and serves it from your infrastructure.
|
|
205
|
-
|
|
206
|
-
Deploy your project with the Zuplo CLI. The `project` field in `zuplo.jsonc`
|
|
207
|
-
selects the project, and Zuplo routes the deployment to your cluster:
|
|
208
|
-
|
|
209
|
-
```bash
|
|
210
|
-
export ZUPLO_API_KEY='<your Zuplo API key>'
|
|
211
|
-
|
|
212
|
-
npx zuplo deploy
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Zuplo compiles the project, then hands the compiled bundle to your cluster's
|
|
216
|
-
management API, which builds the container image and rolls it out.
|
|
217
|
-
|
|
218
|
-
While the command runs, watch the build Job in your cluster:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
kubectl get jobs -n zuplo-system -w
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
```
|
|
225
|
-
NAME STATUS COMPLETIONS DURATION AGE
|
|
226
|
-
build-basic-main-ca9748b-ztcwzdd Running 0/1 32s 32s
|
|
227
|
-
build-basic-main-ca9748b-ztcwzdd Complete 1/1 58s 63s
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
Confirm that the deployment uses an image from your registry:
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
kubectl get deploy -n zuplo -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[0].image}{"\n"}{end}'
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
```
|
|
237
|
-
basic-main-ca9748b us-docker.pkg.dev/acme-corp/zuplo-gateways/basic-main-ca9748b:20260806-172436
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Ask the management API what it is serving:
|
|
241
|
-
|
|
242
|
-
```bash
|
|
243
|
-
curl -H "Authorization: Bearer $ZUPLO_API_KEY" \
|
|
244
|
-
https://zuplo-admin.example.com/v1/deployments
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
```json
|
|
248
|
-
{
|
|
249
|
-
"data": [
|
|
250
|
-
{
|
|
251
|
-
"projectName": "basic",
|
|
252
|
-
"deploymentName": "basic-main-ca9748b-haproxy",
|
|
253
|
-
"deploymentUrl": "https://basic-main-ca9748b.api.example.com"
|
|
254
|
-
}
|
|
255
|
-
]
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Call the deployed API:
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
curl https://basic-main-ca9748b.api.example.com/hello
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
The first request to a deployment can fail while cert-manager issues its
|
|
266
|
-
certificate. Retry the request for up to a minute.
|
|
267
|
-
|
|
268
|
-
:::note
|
|
269
|
-
|
|
270
|
-
The CLI can misreport the status of deployments to self-hosted clusters. See
|
|
271
|
-
[Troubleshooting](./troubleshooting.md#failed-to-deploy-the-environment-but-the-cluster-shows-the-build-succeeding).
|
|
272
|
-
Use the in-cluster checks to confirm the result.
|
|
273
|
-
|
|
274
|
-
:::
|
|
275
|
-
|
|
276
|
-
## Resolve a failed check
|
|
277
|
-
|
|
278
|
-
[Troubleshooting](./troubleshooting.md) lists errors from these checks and their
|
|
279
|
-
causes.
|