Compare commits

...

69 Commits

Author SHA1 Message Date
cyberslack_lee
c8b82797c5 fix: harden cluster key rate limit cookie parsing (#4091) 2026-07-23 10:25:37 +08:00
EndlessSeeker
b272f3a818 docs: add CNCF incubation review artifacts (#4177)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-07-22 18:09:49 +08:00
Xijun Dai
762324c376 fix(ai-proxy): fix cache token double-counting in Claude-to-OpenAI protocol conversion (#4149)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
Co-authored-by: Kent Dong <ch3cho@qq.com>
2026-07-18 00:14:24 +08:00
geekspeng
165f2974db feat(ai-token,cluster-key-ratelimit): multi-rule OR overlay (breaking change) (#4011)
Signed-off-by: geekspeng <geekspeng@icloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 21:11:33 +08:00
ljbddy
2682c926fc feat(ai-statistics): add llm_failure_count metric for error response tracking (#3975)
Signed-off-by: ljb <1327941766@qq.com>
2026-07-17 19:16:16 +08:00
Anas Khan
e01db53e9f fix(cache-control): emit standard max-age Cache-Control directive (#4118)
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
2026-07-17 18:42:30 +08:00
EndlessSeeker
45942d90b6 fix(jwt-auth): harden cookie token parsing (#4153)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-07-17 14:23:19 +08:00
Kent Dong
58773cb98e feat(ci): allow custom wasm plugin image name via .buildrc (#4053)
Signed-off-by: CH3CHO <ch3cho@qq.com>
2026-07-16 14:41:15 +08:00
EndlessSeeker
afb72b81cb test: add Gateway API v1.4 conformance CI (#4135)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-07-14 19:49:58 +08:00
澄潭
152f14b171 fix(golang-filter): align Envoy Go bindings with the envoy/envoy submodule (#4138)
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-07-14 17:53:39 +08:00
澄潭
e169ccbe5f feat: require HTTP Basic auth for higress-ops MCP server (#4139)
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-07-14 17:51:28 +08:00
cyberslack_lee
154782660c fix: fix rand.init(0) panic in failover.go (#4112) 2026-07-09 14:41:33 +08:00
澄潭
e8b9989265 fix: align cors plugin behavior with cors semantics (#4036)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-07-09 14:36:08 +08:00
澄潭
c6fa1f2ad0 fix: bump envoy submodule to skip Redis async client teardown on unchanged config (#4104)
Signed-off-by: 澄潭 <zty98751@alibaba-inc.com>
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-07-08 10:05:14 +08:00
澄潭
bd9c4c5104 Add Envoy gateway update skill (#4105)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-07-07 23:26:38 +08:00
澄潭
4e4ca39b42 fix: bump envoy submodule for doAfterVmCallActions drain-to-local (#4034) (#4080)
Signed-off-by: 澄潭 <zty98751@alibaba-inc.com>
2026-07-07 09:36:25 +08:00
澄潭
d5f2b624fb feat: initialize issue-spec workflow for Higress (#4059)
Signed-off-by: johnlanni <johnlanni@users.noreply.github.com>
Co-authored-by: johnlanni <johnlanni@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 21:55:53 +08:00
澄潭
6c98ea74e0 fix: avoid waiting for missing request bodies in transformer (#4024)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-06-25 17:36:01 +08:00
EndlessSeeker
39ec41aab6 Release v2.2.3 (#4019)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-25 10:34:30 +08:00
澄潭
910c26ec14 chore: update envoy submodule (#4017)
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-06-24 19:06:16 +08:00
EndlessSeeker
582af464c0 Revert "fix: Skip TLS certificate verification for HTTPS upstreams" (#4016)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-24 15:39:32 +08:00
EndlessSeeker
04389fd0c6 fix: harden cluster key rate limit cookie parsing (#4012)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-24 15:03:05 +08:00
EndlessSeeker
10498a2c86 fix(oidc): fail closed when verifier is unavailable (#4013)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
Co-authored-by: Jingze <52855280+Jing-ze@users.noreply.github.com>
2026-06-24 14:22:24 +08:00
yangjuan
e958290283 feat(ai-context-limit): add context window limit wasm plugin (#4000)
Signed-off-by: Cai Rui <cairui@U-7VTK6WQN-2207.local>
2026-06-24 13:40:51 +08:00
DENG
6d1c55c1fb fix(ai-proxy): add ids for Vertex tool calls (#3990)
Signed-off-by: DENG <33118163+XinhhD@users.noreply.github.com>
Co-authored-by: woody <yaodiwu618@gmail.com>
2026-06-23 20:34:54 +08:00
9r0k
e57f18e581 feat(ai-proxy): vLLM passthrough for Anthropic Messages and newer OpenAI endpoints (#3989)
Signed-off-by: Rand01ph <tanyawei1991@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: woody <yaodiwu618@gmail.com>
2026-06-23 19:34:54 +08:00
Kent Dong
1694f48fd9 fix(ai-cache): handle SSE first chunk with role only (fixes #3953) (#3962)
Signed-off-by: CH3CHO <ch3cho@qq.com>
Co-authored-by: woody <yaodiwu618@gmail.com>
2026-06-23 17:38:50 +08:00
cyberslack_lee
7c94b5a822 fix: fix some typo errors (#3988)
Co-authored-by: woody <yaodiwu618@gmail.com>
2026-06-23 11:08:44 +08:00
geekspeng
ab0085362c fix(helm): apply imagePullPolicy to controller container and promtail sidecar (#4002)
Signed-off-by: geekspeng <geekspeng@icloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 22:33:03 +08:00
zijiren
9c13b6418c feat: Enhance SSL passthrough support (#3943)
Signed-off-by: zijiren233 <pyh1670605849@gmail.com>
2026-06-22 21:06:42 +08:00
EndlessSeeker
f060c9f51d fix: preserve ingress load balancer hostnames (#3994)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-22 11:34:38 +08:00
Heracles
bd6039f53b fix(helm): use Chart.Version as default plugin-server image tag (#3998)
Signed-off-by: yqz <2678785492@qq.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-22 09:28:36 +08:00
小画家
36bd830029 bugfix: fix golang-filter architecture mismatch during local gateway build on arm64 macOS (#3905)
Signed-off-by: 胥寅 <xuuyin@dingtalk.com>
Co-authored-by: 胥寅 <xuuyin@dingtalk.com>
2026-06-21 18:13:58 +08:00
澄潭
4db4633560 chore: update envoy submodule (#3987)
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-06-17 20:28:15 +08:00
EndlessSeeker
efaef2e3d0 Support configurable GatewayClass isolation (#3981)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-17 20:27:45 +08:00
geekspeng
e758504d72 fix(helm): move imagePullPolicy from PodSpec to container level in gateway pod template (#3924)
Signed-off-by: geekspeng <geekspeng@icloud.com>
2026-06-17 16:22:51 +08:00
DENG
3c3a067f07 fix(ai-proxy): include tool call type in Claude stream deltas (#3985)
Signed-off-by: DENG <33118163+XinhhD@users.noreply.github.com>
Co-authored-by: woody <yaodiwu618@gmail.com>
2026-06-17 12:18:18 +08:00
DENG
7e11e2f320 fix(ai-proxy): preserve Vertex thoughtSignature in OpenAI tool calls (#3973)
Signed-off-by: DENG <33118163+XinhhD@users.noreply.github.com>
2026-06-17 11:40:38 +08:00
DENG
7abf27a2a3 fix(ai-proxy): clean client Authorization header for api-key providers (#3956)
Signed-off-by: DENG <33118163+XinhhD@users.noreply.github.com>
Co-authored-by: woody <yaodiwu618@gmail.com>
2026-06-16 16:19:37 +08:00
EndlessSeeker
d1f95f6e54 fix: set gateway status address type by value (#3980)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-16 13:41:25 +08:00
EndlessSeeker
83c12d571d feat(helm): allow skipping IngressClass creation (#3979)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-16 11:26:56 +08:00
Jingze
45fc5a31bc test(wasm-plugins): lift unit-test coverage to ≥90% across 9 plugins (#3879)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: woody <yaodiwu618@gmail.com>
2026-06-15 20:33:10 +08:00
Jingze
3065d4e071 test(traffic-tag): cover operator matrix, weight CDF boundaries, and parser reject paths (#3878)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
2026-06-15 20:32:38 +08:00
Jingze
c65d4cd938 test(hmac-auth-apisix): cover algorithm matrix, clock skew, and auth edge cases (#3875)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 20:31:02 +08:00
Jingze
547b7bf45a test(transformer): add end-to-end coverage and fix silent error wrapping (#3873)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
2026-06-15 20:29:51 +08:00
Jingze
bf0b1e96c5 test(wasm-go/mcp): expand unit test coverage for mcp-server framework (#3871)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 20:29:20 +08:00
EndlessSeeker
c69526b30e fix(helm): disable alpha Gateway API watches by default (#3971)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-06-15 18:52:28 +08:00
EndlessSeeker
73e7510eca fix: merge InferencePool route configs during HTTPRoute merge (#3964) 2026-06-12 18:05:24 +08:00
yangjuan
2897c1e83a feat(model-router): add keepOriginalModelName option to preserve full model name (#3916)
Signed-off-by: Cai Rui <yangjuan.cr@alibaba-inc.com>
2026-06-07 15:49:45 +08:00
FAUST
b69b5c7c10 doc: add missed chinese readme for extensions (#3890)
Signed-off-by: zhoujinyu <2319109590@qq.com>
2026-06-07 12:26:08 +08:00
Jingze
57db9ccc46 fix(oidc): fix Set-Cookie header corruption during session refresh (#3928)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-05 17:58:20 +08:00
澄潭
137d76332e fix: rebuild mcp filter on high memory usage (#3922)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-06-05 10:57:43 +08:00
澄潭
d888ab23de fix(wasm): remove request-count rebuild triggers (#3923)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-06-05 10:57:24 +08:00
Jingze
29b5a533e6 fix(oidc): bump oauth2-proxy to fix nil panic in verifier callback (#3914)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-04 10:52:24 +08:00
Jingze
071086904d fix(ai-proxy): strip anthropic-beta and anthropic-version headers in vertex passthrough (#3904)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
2026-06-02 20:26:30 +08:00
JianweiWang
58ffdae6ba feat(ai-security-guard): add Embedding API content detection support (#3895)
Signed-off-by: root <jianwei.wjw@alibaba-inc.com>
2026-06-02 12:11:00 +08:00
JianweiWang
a45b1dde6c fix(ai-statistics): skip empty streaming model values (#3851)
Signed-off-by: jianwei.wjw <jianwei.wjw@alibaba-inc.com>
2026-06-02 12:10:46 +08:00
zat366
52c99eb27d feat(ai-load-balancer): add cluster_hash load balancing policy with FNV-1a consistent hashing (#3898)
Signed-off-by: zat366 <authentic.zhao@gmail.com>
2026-06-01 10:19:46 +08:00
JianweiWang
c21a38e783 feat(ai-security-guard): structured x_higress deny response, error-path metrics, and AI logging (#3894)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: rinfx <yucheng.lxr@alibaba-inc.com>
2026-05-29 10:45:10 +08:00
Jingze
385f8d8b4e fix(ai-proxy): natively route Anthropic Messages to vertex :rawPredict (#3860)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:54:56 +08:00
Betula-L
a86aaadaa4 feat(jwt-auth): support remote JWKS (#3838)
Signed-off-by: Betula-L <6059935+Betula-L@users.noreply.github.com>
Co-authored-by: Betula-L <6059935+Betula-L@users.noreply.github.com>
2026-05-25 16:04:10 +08:00
Kent Dong
e6fc09b14f fix: Fix test cases in registry/nacos/mcpserver/watcher_test.go (#3869)
Signed-off-by: CH3CHO <ch3cho@qq.com>
2026-05-25 15:15:03 +08:00
Kent Dong
b8ff815100 fix: Fix the description of basePath related fields of ai-proxy plugin (#3870)
Signed-off-by: CH3CHO <ch3cho@qq.com>
2026-05-25 15:02:39 +08:00
zat366
632c6ca655 fix(ai-proxy): use HasSuffix instead of Contains in claude.GetApiName to prevent sub-path misidentification (#3839)
Signed-off-by: zat366 <authentic.zhao@gmail.com>
2026-05-25 14:34:45 +08:00
Ghxst
8a0f8a8208 feature: support plural key-auth credentials (#3849)
Signed-off-by: GHX5T-SOL <200635707+GHX5T-SOL@users.noreply.github.com>
Co-authored-by: GHX5T-SOL <200635707+GHX5T-SOL@users.noreply.github.com>
2026-05-24 17:56:58 +08:00
Jingze
b99e0c5027 fix(ai-proxy): re-anchor X-HI-ORIGINAL-AUTH on first hop to fix cascaded-gateway 401 (#3864)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:01:08 +08:00
muicoder
34040d1b99 fix #3858 arm64 image corruption caused by overriding BuildKit TARGETARCH (#3859)
Signed-off-by: muicoder <muicoder@gmail.com>
2026-05-22 11:17:56 +08:00
github-actions[bot]
870ed27435 Add release notes (#3854)
Co-authored-by: EndlessSeeker <153817598+EndlessSeeker@users.noreply.github.com>
2026-05-21 17:40:55 +08:00
EndlessSeeker
1ccafab24b Release v2.2.2 (#3852)
Signed-off-by: 如漫 <zhaoyuanxiao.zyx@alibaba-inc.com>
Signed-off-by: EndlessSeeker <1766508902@qq.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-21 14:50:10 +08:00
308 changed files with 29909 additions and 1140 deletions

View File

@@ -0,0 +1,94 @@
---
name: higress-update-envoy-gateway
description: Update Higress Envoy binary and gateway image dependencies for e2e validation. Use when Codex needs to build Envoy packages from the current Higress branch, upload those packages to higress-group/proxy releases, update Makefile.core.mk ENVOY_PACKAGE_URL_PATTERN and ENVOY_LATEST_IMAGE_TAG, run make build-gateway-local, retag the generated proxy/proxyv2 image as gateway, push the gateway image, or prepare a signed-off PR that lets Higress e2e tests consume a new Envoy build.
---
# Higress Envoy Gateway Dependency Update
## Core Workflow
Run from the Higress repo root unless a step explicitly says otherwise.
1. Verify context:
- Check `git status --short --branch`.
- Do not remove unrelated user changes or generated artifacts.
- Use `gh` for GitHub release/PR checks and always pass `--repo` for non-current repos.
2. Build Envoy packages from the current branch:
```bash
git submodule update --init
make build-envoy
```
Expected artifacts land in `external/package/`, typically:
- `envoy-alpha-<proxy-sha>.tar.gz`
- `envoy-symbol-<proxy-sha>.tar.gz`
- matching `.sha256` and `.dwp` files
3. Publish Envoy package release in `higress-group/proxy`:
- Create a new release tag by incrementing the requested RC/test tag, for example `v2.2.4-rc.2-test-cpp-host`.
- Match the reference release asset names, even if local filenames include SHAs:
- local `envoy-alpha-<sha>.tar.gz` uploads as `envoy-amd64.tar.gz`
- local `envoy-symbol-<sha>.tar.gz` uploads as `envoy-symbol-amd64.tar.gz`
- Use temporary renamed copies rather than renaming source artifacts:
```bash
cp external/package/envoy-alpha-<sha>.tar.gz /tmp/envoy-amd64.tar.gz
cp external/package/envoy-symbol-<sha>.tar.gz /tmp/envoy-symbol-amd64.tar.gz
gh release create <release-tag> /tmp/envoy-amd64.tar.gz /tmp/envoy-symbol-amd64.tar.gz \
--repo higress-group/proxy --target <target-branch> --title <release-tag> --generate-notes
gh release view <release-tag> --repo higress-group/proxy --json tagName,targetCommitish,assets,url
```
- If following an existing reference release, inspect it first with `gh release view`.
4. Update Higress Makefile dependencies:
- In `Makefile.core.mk`, set:
```make
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/<release-tag>/envoy-symbol-ARCH.tar.gz
```
- After building and pushing the gateway image, set:
```make
ENVOY_LATEST_IMAGE_TAG ?= <gateway-image-tag>
```
5. Build the local gateway image:
```bash
make build-gateway-local
```
Watch the log to confirm Envoy downloads from the new release URL. The build target may emit an image under a proxy-style repository/name, commonly:
- `higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/proxyv2:<tag>`
6. Retag proxy image as gateway:
```bash
docker tag higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/proxyv2:<tag> \
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:<tag>
docker images --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedSince}}' \
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway
```
Verify the `gateway:<tag>` and `proxyv2:<tag>` image IDs match.
7. Push the gateway image when requested:
```bash
docker push higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:<tag>
```
Record the pushed digest in the final response.
8. Commit and push Makefile changes:
- Commit only intended tracked files. Do not add `plugins/golang-filter/golang-filter_amd64.so` or other build outputs unless explicitly requested.
- Use DCO sign-off:
```bash
git add Makefile.core.mk
git commit -s -m "Update gateway envoy dependencies"
git push origin <branch>
```
- If DCO fails after a previous unsigned commit:
```bash
git commit --amend --no-edit --signoff
git push --force-with-lease origin <branch>
gh pr checks <pr-number> --repo higress-group/higress
```
## Practical Notes
- `make build-gateway-local` may need Docker daemon access, network access, and write access to repo/submodule state.
- If sandboxed commands fail with read-only filesystem errors, Docker socket permission errors, or network failures, rerun the same important command with elevated permissions and a concrete justification.
- The default local gateway tag usually comes from the current Git revision shown in the build log as `TAG=<sha>`.
- For e2e validation, the point of this workflow is to make `install-dev`, `install-dev-wasmplugin`, and local image update targets use the newly pushed gateway image through `ENVOY_LATEST_IMAGE_TAG`.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Higress Envoy Gateway"
short_description: "Update Envoy packages and gateway tag"
default_prompt: "Use $higress-update-envoy-gateway to update Higress Makefile Envoy dependencies and build a gateway image for e2e validation."

View File

@@ -0,0 +1,35 @@
---
name: issue-spec-apply
description: Implement PROCESS comments for an issue-spec change and keep PR traceability synchronized.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Apply
Use when the user asks for /issue-spec:apply, issue-spec apply, or implementing PROCESS/TASK scopes from an issue-spec change.
## Steps
1. Read proposal/design/implement issue context and list typed comments with issue-spec comment list --json.
2. Confirm issue-spec auth status --json includes the expected GitHub backend. Local gh-authenticated sessions can use the native gh backend; keep ISSUE_SPEC_TOKEN="$(gh auth token)" only as an older-version or forced-rest compatibility path.
3. Create or update PROCESS comments with owner agent, scope, dependencies, write ownership, and status.
4. Split non-trivial work into independent worker PROCESS nodes when file/module ownership does not overlap; execute independent workers in parallel when available.
5. Add dedicated review PROCESS nodes for non-trivial changes. Review PROCESS nodes should own review scopes such as CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
6. Link each PROCESS to its TASK comments with issue-spec link.
7. Implement the code changes for one PROCESS scope at a time, or integrate completed worker outputs by dependency order.
8. Link every worker and review PROCESS to the PR with issue-spec pr link-process.
9. Add PR rationale comments on key changed lines with issue-spec pr rationale, each linked to a SPEC comment.
10. Mark PROCESS comments done only after implementation/review work and focused verification evidence exist.
## Coordinator DAG Execution
1. Build the ready set from PROCESS nodes whose dependencies are done.
2. Keep immediate blocking work local when the next step depends on it.
3. Spawn or assign independent worker agents only when their write ownership is disjoint.
4. Spawn or assign independent review agents only when their review scopes are disjoint.
5. Integrate completed outputs by dependency order and update PROCESS evidence before marking done.

View File

@@ -0,0 +1,24 @@
---
name: issue-spec-archive
description: Create the post-merge durable spec archive PR for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Archive
Use when the user asks for /issue-spec:archive, issue-spec archive, or creating the post-merge durable spec PR.
## Steps
1. Confirm the implementation PR is merged.
2. Create the durable spec PR:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --create-pr --branch issue-spec/durable-spec-<capability> --json
3. Review the durable spec PR for long-lived behavior only. Do not copy process records, review findings, or verification logs into durable specs.
4. After durable spec PR merge, keep proposal/design/implement issues as audit history unless the project policy says to close them.

View File

@@ -0,0 +1,58 @@
---
name: issue-spec-github
description: Use GitHub CLI for GitHub issues, pull requests, CI runs, and API queries that issue-spec does not wrap.
license: MIT
compatibility: Requires GitHub CLI (gh).
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# GitHub CLI
Use the `gh` CLI to interact with GitHub repositories, issues, pull requests, CI, and API endpoints.
## When To Use
- Checking PR status, reviews, mergeability, or CI checks.
- Creating, viewing, updating, closing, or commenting on GitHub issues.
- Listing or inspecting pull requests, workflow runs, releases, labels, or repository metadata.
- Calling GitHub API endpoints with `gh api` when issue-spec does not provide a dedicated command.
## When Not To Use
- Local git operations such as commit, branch, fetch, merge, or push. Use `git` directly.
- Non-GitHub repositories. Use the matching provider CLI instead.
- Complex code review across local diffs. Read the repository files directly and use issue-spec review commands for traceable findings.
## Setup
```bash
gh auth login
gh auth status
```
## Common Commands
```bash
gh issue list --repo owner/repo --state open
gh issue view 42 --repo owner/repo --json number,title,state,url,body
gh issue comment 42 --repo owner/repo --body "Comment body"
gh pr list --repo owner/repo
gh pr view 17 --repo owner/repo --json number,title,state,headRefName,baseRefName,url
gh pr checks 17 --repo owner/repo
gh run list --repo owner/repo --limit 10
gh run view <run-id> --repo owner/repo --log-failed
gh api repos/owner/repo/labels --jq '.[].name'
```
## Notes
- Always pass `--repo owner/repo` when the current directory is not definitely inside the target repository.
- Use GitHub URLs directly when convenient, for example `gh pr view https://github.com/owner/repo/pull/17`.
- Prefer structured output with `--json` and `--jq` when another command or agent step consumes the result.
- issue-spec owns the proposal, design, implement, typed comment, review, verify, and archive workflow state. Use `gh` for adjacent GitHub operations that are outside issue-spec's command surface.

View File

@@ -0,0 +1,37 @@
---
name: issue-spec-propose
description: Create or continue proposal, SPEC, QUESTION, design, and TASK artifacts for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Propose
Use when the user asks for /issue-spec:propose, issue-spec propose, creating a change proposal, drafting SPEC comments, or preparing design/tasks after questions converge.
## Steps
1. Create the proposal issue:
issue-spec issue create proposal --repo higress-group/higress --change <change-name> --body-file <proposal.md>
2. If the proposal body needs revision after discussion, update it in place:
issue-spec issue update --repo higress-group/higress --issue <proposal-issue> --body-file <proposal.md> --summary "<what changed>"
3. Add SPEC comments with issue-spec comment upsert --type SPEC. SPEC comments must use MUST/SHALL and WHEN/THEN scenarios.
4. Add QUESTION comments for unresolved behavior with issue-spec question create and resolve blocking questions before design.
5. Create the design issue after SPEC/QUESTION convergence:
issue-spec issue create design --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --body-file <design.md>
6. Add TASK comments with issue-spec comment upsert --type TASK and link every TASK to covered SPEC comments with issue-spec link.
7. Create the implement issue once tasks are ready:
issue-spec issue create implement --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --design <design-issue-or-url> --body-file <implement.md>
8. Run issue-spec verify-links and fix missing backlinks before implementation.

View File

@@ -0,0 +1,32 @@
---
name: issue-spec-review
description: Review an issue-spec implementation PR, create PR line findings, reply after fixes, and sync REVIEW comments.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Review
Use when the user asks for /issue-spec:review, issue-spec review, or a PR review gate for an issue-spec implementation.
## Steps
1. Run issue-spec review sync --repo higress-group/higress --pr <number> --implement <issue> --id REVIEW-<n> --json to capture current rationale comments, findings, and checks.
2. For non-trivial PRs, spawn or assign dedicated review agents as review PROCESS owners. Multiple review agents can run in parallel when their review scopes are independent.
3. Give each review agent a concrete scope and expected output: actionable findings only, severity, file/line, linked SPEC, owner PROCESS, and suggested fix.
4. Create actionable PR line findings with issue-spec review finding. Use P0/P1 for blockers and P2 for non-blocking follow-up.
5. Assign every finding to a PROCESS owner. If no findings are found, record that result in REVIEW or VERIFY evidence.
6. After the worker fixes a finding, reply to the original thread with issue-spec review reply --status resolved.
7. Re-run review sync. P0/P1 findings must be resolved before final verify/archive.
## Review DAG Policy
1. Every non-trivial PR should have at least one dedicated review PROCESS node before final verify.
2. Use multiple review agents in parallel when scopes are independent, for example CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
3. A review agent reports findings only; the coordinator converts actionable line findings into issue-spec review finding comments.
4. P0/P1 findings block final verify until the owner PROCESS fixes them and issue-spec review reply records the resolution on the original thread.
5. If a review agent finds no issues, record that result in REVIEW or VERIFY evidence before marking the review PROCESS done.

View File

@@ -0,0 +1,28 @@
---
name: issue-spec-verify
description: Run final issue-spec verification across traceability, questions, review findings, PR rationale, PR checks, and durable spec draft.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Verify
Use when the user asks for /issue-spec:verify, issue-spec verify, or final readiness evidence before merge/archive.
## Steps
1. Run focused project tests and record evidence in VERIFY comments.
2. Run issue-spec verify-links --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json.
3. Render a durable spec draft:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --output /tmp/<capability>-spec.md --json
4. Run final verify:
issue-spec verify --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --pr <pr> --durable-spec /tmp/<capability>-spec.md --json
5. Final verify must fail if blocking questions, missing links, missing PROCESS rationale, open P0/P1 findings, failed or pending PR checks, or durable spec omissions exist.

View File

@@ -0,0 +1,53 @@
---
name: issue-spec-workflow
description: Use issue-spec to run an issue-native OpenSpec-style workflow with GitHub issues, typed comments, PR review comments, final verification, and durable spec archive PRs.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Workflow
Use this skill for issue-native OpenSpec work. Active change artifacts live in GitHub issues and issue comments; durable specs are repository files created after implementation merge.
## Start
1. Run issue-spec auth status --json and confirm the active auth source and GitHub backend.
2. Run issue-spec status --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json when issues already exist.
3. For new work, create proposal, design, and implement issues with issue-spec issue create and pass --body-file with concrete markdown content.
4. When an issue body changes, update it in place with issue-spec issue update --body-file and include --summary for the human-readable audit trail.
5. Store requirements, tasks, process ownership, review, and verify evidence as typed comments.
## GitHub Backend
- Local agents may rely on native GitHub CLI support: when no ISSUE_SPEC_TOKEN, GH_TOKEN, GITHUB_TOKEN, keyring token, or issue-spec config token is present and gh auth status --active succeeds for the target host, issue-spec auto-selects the gh backend.
- Explicit env or stored issue-spec tokens keep the rest backend under auto selection. Set ISSUE_SPEC_GITHUB_BACKEND=rest or ISSUE_SPEC_GITHUB_BACKEND=gh only when a workflow needs deterministic backend selection.
- The gh backend proxies GitHub API operations through gh api and uses gh --hostname for Enterprise hosts. It does not replace local git commands.
- ISSUE_SPEC_API_URL applies to the rest backend. Forced gh mode should be used only with hosts that gh can address.
- Use ISSUE_SPEC_TOKEN="$(gh auth token)" only for older issue-spec versions or when deliberately forcing rest while sourcing the token from gh.
## Rules
- Create SPEC comments before design; each SPEC must be testable and include WHEN/THEN scenarios.
- Do not leave active proposal/design/implement issue bodies as TBD placeholders.
- Resolve blocking QUESTION comments before design/tasks, or explicitly record accepted assumptions.
- Link SPEC <-> TASK and TASK <-> PROCESS with issue-spec link.
- Link every PROCESS to the implementation PR with issue-spec pr link-process.
- For non-trivial changes, include review PROCESS nodes in the DAG; review agents are scheduled like worker agents and can run in parallel when their review scopes are independent.
- Small changes may stay coordinator-only, but record the serial execution decision in the implement or VERIFY evidence.
- Before human review, add PR rationale comments with issue-spec pr rationale for every active PROCESS.
- Use issue-spec review finding for PR line findings and issue-spec review reply to close the original thread.
- Run issue-spec review sync and issue-spec verify before declaring ready.
- After the implementation PR merges, create the separate durable spec PR with issue-spec archive durable-spec --create-pr.
## Coordinator DAG Execution
1. Treat PROCESS comments as DAG nodes with explicit owner, dependencies, write or review scope, PR link, and evidence.
2. Select ready PROCESS nodes whose dependencies are done and whose scopes do not overlap.
3. Dispatch independent worker PROCESS nodes in parallel when their file/module ownership is disjoint.
4. Dispatch independent review PROCESS nodes in parallel for non-trivial PRs after PR rationale exists.
5. Integrate completed worker outputs by dependency order; route P0/P1 review findings back to the owner PROCESS.
6. Mark PROCESS nodes done only after their implementation or review evidence is recorded and blocking findings are resolved.

View File

@@ -0,0 +1,31 @@
---
name: "Issue Spec: Apply"
description: "Implement PROCESS comments for an issue-spec change and keep PR traceability synchronized."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Apply
Use when the user asks for /issue-spec:apply, issue-spec apply, or implementing PROCESS/TASK scopes from an issue-spec change.
## Steps
1. Read proposal/design/implement issue context and list typed comments with issue-spec comment list --json.
2. Confirm issue-spec auth status --json includes the expected GitHub backend. Local gh-authenticated sessions can use the native gh backend; keep ISSUE_SPEC_TOKEN="$(gh auth token)" only as an older-version or forced-rest compatibility path.
3. Create or update PROCESS comments with owner agent, scope, dependencies, write ownership, and status.
4. Split non-trivial work into independent worker PROCESS nodes when file/module ownership does not overlap; execute independent workers in parallel when available.
5. Add dedicated review PROCESS nodes for non-trivial changes. Review PROCESS nodes should own review scopes such as CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
6. Link each PROCESS to its TASK comments with issue-spec link.
7. Implement the code changes for one PROCESS scope at a time, or integrate completed worker outputs by dependency order.
8. Link every worker and review PROCESS to the PR with issue-spec pr link-process.
9. Add PR rationale comments on key changed lines with issue-spec pr rationale, each linked to a SPEC comment.
10. Mark PROCESS comments done only after implementation/review work and focused verification evidence exist.
## Coordinator DAG Execution
1. Build the ready set from PROCESS nodes whose dependencies are done.
2. Keep immediate blocking work local when the next step depends on it.
3. Spawn or assign independent worker agents only when their write ownership is disjoint.
4. Spawn or assign independent review agents only when their review scopes are disjoint.
5. Integrate completed outputs by dependency order and update PROCESS evidence before marking done.

View File

@@ -0,0 +1,20 @@
---
name: "Issue Spec: Archive"
description: "Create the post-merge durable spec archive PR for an issue-spec change."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Archive
Use when the user asks for /issue-spec:archive, issue-spec archive, or creating the post-merge durable spec PR.
## Steps
1. Confirm the implementation PR is merged.
2. Create the durable spec PR:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --create-pr --branch issue-spec/durable-spec-<capability> --json
3. Review the durable spec PR for long-lived behavior only. Do not copy process records, review findings, or verification logs into durable specs.
4. After durable spec PR merge, keep proposal/design/implement issues as audit history unless the project policy says to close them.

View File

@@ -0,0 +1,33 @@
---
name: "Issue Spec: Propose"
description: "Create or continue proposal, SPEC, QUESTION, design, and TASK artifacts for an issue-spec change."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Propose
Use when the user asks for /issue-spec:propose, issue-spec propose, creating a change proposal, drafting SPEC comments, or preparing design/tasks after questions converge.
## Steps
1. Create the proposal issue:
issue-spec issue create proposal --repo higress-group/higress --change <change-name> --body-file <proposal.md>
2. If the proposal body needs revision after discussion, update it in place:
issue-spec issue update --repo higress-group/higress --issue <proposal-issue> --body-file <proposal.md> --summary "<what changed>"
3. Add SPEC comments with issue-spec comment upsert --type SPEC. SPEC comments must use MUST/SHALL and WHEN/THEN scenarios.
4. Add QUESTION comments for unresolved behavior with issue-spec question create and resolve blocking questions before design.
5. Create the design issue after SPEC/QUESTION convergence:
issue-spec issue create design --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --body-file <design.md>
6. Add TASK comments with issue-spec comment upsert --type TASK and link every TASK to covered SPEC comments with issue-spec link.
7. Create the implement issue once tasks are ready:
issue-spec issue create implement --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --design <design-issue-or-url> --body-file <implement.md>
8. Run issue-spec verify-links and fix missing backlinks before implementation.

View File

@@ -0,0 +1,28 @@
---
name: "Issue Spec: Review"
description: "Review an issue-spec implementation PR, create PR line findings, reply after fixes, and sync REVIEW comments."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Review
Use when the user asks for /issue-spec:review, issue-spec review, or a PR review gate for an issue-spec implementation.
## Steps
1. Run issue-spec review sync --repo higress-group/higress --pr <number> --implement <issue> --id REVIEW-<n> --json to capture current rationale comments, findings, and checks.
2. For non-trivial PRs, spawn or assign dedicated review agents as review PROCESS owners. Multiple review agents can run in parallel when their review scopes are independent.
3. Give each review agent a concrete scope and expected output: actionable findings only, severity, file/line, linked SPEC, owner PROCESS, and suggested fix.
4. Create actionable PR line findings with issue-spec review finding. Use P0/P1 for blockers and P2 for non-blocking follow-up.
5. Assign every finding to a PROCESS owner. If no findings are found, record that result in REVIEW or VERIFY evidence.
6. After the worker fixes a finding, reply to the original thread with issue-spec review reply --status resolved.
7. Re-run review sync. P0/P1 findings must be resolved before final verify/archive.
## Review DAG Policy
1. Every non-trivial PR should have at least one dedicated review PROCESS node before final verify.
2. Use multiple review agents in parallel when scopes are independent, for example CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
3. A review agent reports findings only; the coordinator converts actionable line findings into issue-spec review finding comments.
4. P0/P1 findings block final verify until the owner PROCESS fixes them and issue-spec review reply records the resolution on the original thread.
5. If a review agent finds no issues, record that result in REVIEW or VERIFY evidence before marking the review PROCESS done.

View File

@@ -0,0 +1,24 @@
---
name: "Issue Spec: Verify"
description: "Run final issue-spec verification across traceability, questions, review findings, PR rationale, PR checks, and durable spec draft."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Verify
Use when the user asks for /issue-spec:verify, issue-spec verify, or final readiness evidence before merge/archive.
## Steps
1. Run focused project tests and record evidence in VERIFY comments.
2. Run issue-spec verify-links --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json.
3. Render a durable spec draft:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --output /tmp/<capability>-spec.md --json
4. Run final verify:
issue-spec verify --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --pr <pr> --durable-spec /tmp/<capability>-spec.md --json
5. Final verify must fail if blocking questions, missing links, missing PROCESS rationale, open P0/P1 findings, failed or pending PR checks, or durable spec omissions exist.

View File

@@ -0,0 +1 @@
../../.agents/skills/higress-update-envoy-gateway

View File

@@ -0,0 +1,35 @@
---
name: issue-spec-apply
description: Implement PROCESS comments for an issue-spec change and keep PR traceability synchronized.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Apply
Use when the user asks for /issue-spec:apply, issue-spec apply, or implementing PROCESS/TASK scopes from an issue-spec change.
## Steps
1. Read proposal/design/implement issue context and list typed comments with issue-spec comment list --json.
2. Confirm issue-spec auth status --json includes the expected GitHub backend. Local gh-authenticated sessions can use the native gh backend; keep ISSUE_SPEC_TOKEN="$(gh auth token)" only as an older-version or forced-rest compatibility path.
3. Create or update PROCESS comments with owner agent, scope, dependencies, write ownership, and status.
4. Split non-trivial work into independent worker PROCESS nodes when file/module ownership does not overlap; execute independent workers in parallel when available.
5. Add dedicated review PROCESS nodes for non-trivial changes. Review PROCESS nodes should own review scopes such as CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
6. Link each PROCESS to its TASK comments with issue-spec link.
7. Implement the code changes for one PROCESS scope at a time, or integrate completed worker outputs by dependency order.
8. Link every worker and review PROCESS to the PR with issue-spec pr link-process.
9. Add PR rationale comments on key changed lines with issue-spec pr rationale, each linked to a SPEC comment.
10. Mark PROCESS comments done only after implementation/review work and focused verification evidence exist.
## Coordinator DAG Execution
1. Build the ready set from PROCESS nodes whose dependencies are done.
2. Keep immediate blocking work local when the next step depends on it.
3. Spawn or assign independent worker agents only when their write ownership is disjoint.
4. Spawn or assign independent review agents only when their review scopes are disjoint.
5. Integrate completed outputs by dependency order and update PROCESS evidence before marking done.

View File

@@ -0,0 +1,24 @@
---
name: issue-spec-archive
description: Create the post-merge durable spec archive PR for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Archive
Use when the user asks for /issue-spec:archive, issue-spec archive, or creating the post-merge durable spec PR.
## Steps
1. Confirm the implementation PR is merged.
2. Create the durable spec PR:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --create-pr --branch issue-spec/durable-spec-<capability> --json
3. Review the durable spec PR for long-lived behavior only. Do not copy process records, review findings, or verification logs into durable specs.
4. After durable spec PR merge, keep proposal/design/implement issues as audit history unless the project policy says to close them.

View File

@@ -0,0 +1,58 @@
---
name: issue-spec-github
description: Use GitHub CLI for GitHub issues, pull requests, CI runs, and API queries that issue-spec does not wrap.
license: MIT
compatibility: Requires GitHub CLI (gh).
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# GitHub CLI
Use the `gh` CLI to interact with GitHub repositories, issues, pull requests, CI, and API endpoints.
## When To Use
- Checking PR status, reviews, mergeability, or CI checks.
- Creating, viewing, updating, closing, or commenting on GitHub issues.
- Listing or inspecting pull requests, workflow runs, releases, labels, or repository metadata.
- Calling GitHub API endpoints with `gh api` when issue-spec does not provide a dedicated command.
## When Not To Use
- Local git operations such as commit, branch, fetch, merge, or push. Use `git` directly.
- Non-GitHub repositories. Use the matching provider CLI instead.
- Complex code review across local diffs. Read the repository files directly and use issue-spec review commands for traceable findings.
## Setup
```bash
gh auth login
gh auth status
```
## Common Commands
```bash
gh issue list --repo owner/repo --state open
gh issue view 42 --repo owner/repo --json number,title,state,url,body
gh issue comment 42 --repo owner/repo --body "Comment body"
gh pr list --repo owner/repo
gh pr view 17 --repo owner/repo --json number,title,state,headRefName,baseRefName,url
gh pr checks 17 --repo owner/repo
gh run list --repo owner/repo --limit 10
gh run view <run-id> --repo owner/repo --log-failed
gh api repos/owner/repo/labels --jq '.[].name'
```
## Notes
- Always pass `--repo owner/repo` when the current directory is not definitely inside the target repository.
- Use GitHub URLs directly when convenient, for example `gh pr view https://github.com/owner/repo/pull/17`.
- Prefer structured output with `--json` and `--jq` when another command or agent step consumes the result.
- issue-spec owns the proposal, design, implement, typed comment, review, verify, and archive workflow state. Use `gh` for adjacent GitHub operations that are outside issue-spec's command surface.

View File

@@ -0,0 +1,37 @@
---
name: issue-spec-propose
description: Create or continue proposal, SPEC, QUESTION, design, and TASK artifacts for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Propose
Use when the user asks for /issue-spec:propose, issue-spec propose, creating a change proposal, drafting SPEC comments, or preparing design/tasks after questions converge.
## Steps
1. Create the proposal issue:
issue-spec issue create proposal --repo higress-group/higress --change <change-name> --body-file <proposal.md>
2. If the proposal body needs revision after discussion, update it in place:
issue-spec issue update --repo higress-group/higress --issue <proposal-issue> --body-file <proposal.md> --summary "<what changed>"
3. Add SPEC comments with issue-spec comment upsert --type SPEC. SPEC comments must use MUST/SHALL and WHEN/THEN scenarios.
4. Add QUESTION comments for unresolved behavior with issue-spec question create and resolve blocking questions before design.
5. Create the design issue after SPEC/QUESTION convergence:
issue-spec issue create design --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --body-file <design.md>
6. Add TASK comments with issue-spec comment upsert --type TASK and link every TASK to covered SPEC comments with issue-spec link.
7. Create the implement issue once tasks are ready:
issue-spec issue create implement --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --design <design-issue-or-url> --body-file <implement.md>
8. Run issue-spec verify-links and fix missing backlinks before implementation.

View File

@@ -0,0 +1,32 @@
---
name: issue-spec-review
description: Review an issue-spec implementation PR, create PR line findings, reply after fixes, and sync REVIEW comments.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Review
Use when the user asks for /issue-spec:review, issue-spec review, or a PR review gate for an issue-spec implementation.
## Steps
1. Run issue-spec review sync --repo higress-group/higress --pr <number> --implement <issue> --id REVIEW-<n> --json to capture current rationale comments, findings, and checks.
2. For non-trivial PRs, spawn or assign dedicated review agents as review PROCESS owners. Multiple review agents can run in parallel when their review scopes are independent.
3. Give each review agent a concrete scope and expected output: actionable findings only, severity, file/line, linked SPEC, owner PROCESS, and suggested fix.
4. Create actionable PR line findings with issue-spec review finding. Use P0/P1 for blockers and P2 for non-blocking follow-up.
5. Assign every finding to a PROCESS owner. If no findings are found, record that result in REVIEW or VERIFY evidence.
6. After the worker fixes a finding, reply to the original thread with issue-spec review reply --status resolved.
7. Re-run review sync. P0/P1 findings must be resolved before final verify/archive.
## Review DAG Policy
1. Every non-trivial PR should have at least one dedicated review PROCESS node before final verify.
2. Use multiple review agents in parallel when scopes are independent, for example CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
3. A review agent reports findings only; the coordinator converts actionable line findings into issue-spec review finding comments.
4. P0/P1 findings block final verify until the owner PROCESS fixes them and issue-spec review reply records the resolution on the original thread.
5. If a review agent finds no issues, record that result in REVIEW or VERIFY evidence before marking the review PROCESS done.

View File

@@ -0,0 +1,28 @@
---
name: issue-spec-verify
description: Run final issue-spec verification across traceability, questions, review findings, PR rationale, PR checks, and durable spec draft.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Verify
Use when the user asks for /issue-spec:verify, issue-spec verify, or final readiness evidence before merge/archive.
## Steps
1. Run focused project tests and record evidence in VERIFY comments.
2. Run issue-spec verify-links --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json.
3. Render a durable spec draft:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --output /tmp/<capability>-spec.md --json
4. Run final verify:
issue-spec verify --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --pr <pr> --durable-spec /tmp/<capability>-spec.md --json
5. Final verify must fail if blocking questions, missing links, missing PROCESS rationale, open P0/P1 findings, failed or pending PR checks, or durable spec omissions exist.

View File

@@ -0,0 +1,53 @@
---
name: issue-spec-workflow
description: Use issue-spec to run an issue-native OpenSpec-style workflow with GitHub issues, typed comments, PR review comments, final verification, and durable spec archive PRs.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Workflow
Use this skill for issue-native OpenSpec work. Active change artifacts live in GitHub issues and issue comments; durable specs are repository files created after implementation merge.
## Start
1. Run issue-spec auth status --json and confirm the active auth source and GitHub backend.
2. Run issue-spec status --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json when issues already exist.
3. For new work, create proposal, design, and implement issues with issue-spec issue create and pass --body-file with concrete markdown content.
4. When an issue body changes, update it in place with issue-spec issue update --body-file and include --summary for the human-readable audit trail.
5. Store requirements, tasks, process ownership, review, and verify evidence as typed comments.
## GitHub Backend
- Local agents may rely on native GitHub CLI support: when no ISSUE_SPEC_TOKEN, GH_TOKEN, GITHUB_TOKEN, keyring token, or issue-spec config token is present and gh auth status --active succeeds for the target host, issue-spec auto-selects the gh backend.
- Explicit env or stored issue-spec tokens keep the rest backend under auto selection. Set ISSUE_SPEC_GITHUB_BACKEND=rest or ISSUE_SPEC_GITHUB_BACKEND=gh only when a workflow needs deterministic backend selection.
- The gh backend proxies GitHub API operations through gh api and uses gh --hostname for Enterprise hosts. It does not replace local git commands.
- ISSUE_SPEC_API_URL applies to the rest backend. Forced gh mode should be used only with hosts that gh can address.
- Use ISSUE_SPEC_TOKEN="$(gh auth token)" only for older issue-spec versions or when deliberately forcing rest while sourcing the token from gh.
## Rules
- Create SPEC comments before design; each SPEC must be testable and include WHEN/THEN scenarios.
- Do not leave active proposal/design/implement issue bodies as TBD placeholders.
- Resolve blocking QUESTION comments before design/tasks, or explicitly record accepted assumptions.
- Link SPEC <-> TASK and TASK <-> PROCESS with issue-spec link.
- Link every PROCESS to the implementation PR with issue-spec pr link-process.
- For non-trivial changes, include review PROCESS nodes in the DAG; review agents are scheduled like worker agents and can run in parallel when their review scopes are independent.
- Small changes may stay coordinator-only, but record the serial execution decision in the implement or VERIFY evidence.
- Before human review, add PR rationale comments with issue-spec pr rationale for every active PROCESS.
- Use issue-spec review finding for PR line findings and issue-spec review reply to close the original thread.
- Run issue-spec review sync and issue-spec verify before declaring ready.
- After the implementation PR merges, create the separate durable spec PR with issue-spec archive durable-spec --create-pr.
## Coordinator DAG Execution
1. Treat PROCESS comments as DAG nodes with explicit owner, dependencies, write or review scope, PR link, and evidence.
2. Select ready PROCESS nodes whose dependencies are done and whose scopes do not overlap.
3. Dispatch independent worker PROCESS nodes in parallel when their file/module ownership is disjoint.
4. Dispatch independent review PROCESS nodes in parallel for non-trivial PRs after PR rationale exists.
5. Integrate completed worker outputs by dependency order; route P0/P1 review findings back to the owner PROCESS.
6. Mark PROCESS nodes done only after their implementation or review evidence is recorded and blocking findings are resolved.

View File

@@ -7,10 +7,14 @@ assignees: ''
---
**If you are reporting *any* crash or *any* potential security issue, *do not*
open an issue in this repo. Please report the issue via [ASRC](https://security.alibaba.com/)(Alibaba Security Response Center) where the issue will be triaged appropriately.**
**If you are reporting a potential security issue, do not open a public issue.
You must submit the same substantive report through both
[GitHub Private Security Advisories](https://github.com/higress-group/higress/security/advisories/new)
and the [Alibaba Security Response Center](https://security.alibaba.com/), as
required by the project's [security policy](https://github.com/higress-group/higress/blob/main/SECURITY.md).
A crash with no suspected security impact may be reported with this template.**
- [ ] I have searched the [issues](https://github.com/alibaba/higress/issues) of this repository and believe that this is not a duplicate.
- [ ] I have searched the [issues](https://github.com/higress-group/higress/issues) of this repository and believe that this is not a duplicate.
### . Issue Description

View File

@@ -104,11 +104,6 @@ jobs:
push_command=${push_command#\"}
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号确保oras push正常解析
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:latest"
echo "TargetImage=${target_image}"
echo "TargetImageLatest=${target_image_latest}"
cd ${{ github.workspace }}/plugins/wasm-${PLUGIN_TYPE}/extensions/${PLUGIN_NAME}
if [ -f ./.buildrc ]; then
echo 'Found .buildrc file, sourcing it...'
@@ -116,6 +111,21 @@ jobs:
else
echo '.buildrc file not found'
fi
# Resolve custom image short name from .buildrc, fallback to plugin directory name.
# .buildrc may define IMAGE_NAME=xxx to override the output image tag.
IMAGE_NAME=${IMAGE_NAME:-$PLUGIN_NAME}
if ! [[ "$IMAGE_NAME" =~ ^[a-z0-9._-]+$ ]]; then
echo "::error::Invalid IMAGE_NAME '$IMAGE_NAME' in .buildrc — must match [a-z0-9._-]+"
exit 1
fi
echo "IMAGE_NAME=${IMAGE_NAME}"
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${IMAGE_NAME}:${{ env.VERSION }}"
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${IMAGE_NAME}:latest"
echo "TargetImage=${target_image}"
echo "TargetImageLatest=${target_image_latest}"
echo "EXTRA_TAGS=${EXTRA_TAGS}"
if [ "${PLUGIN_TYPE}" == "go" ]; then
command="

View File

@@ -93,8 +93,77 @@ jobs:
gateway-conformance-test:
runs-on: ubuntu-22.04
needs: [build]
env:
GATEWAY_API_TEST_NAMESPACE: gateway-conformance-infra
HIGRESS_TEST_IMAGE_TAG: ${{ github.event.pull_request.head.sha || github.sha }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ env.HIGRESS_TEST_IMAGE_TAG }}
- name: Disable containerd image store
run: |
sudo bash -c 'cat > /etc/docker/daemon.json << EOF
{
"features": {
"containerd-snapshotter": false
}
}
EOF'
sudo systemctl restart docker
docker info -f '{{ .DriverStatus }}'
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Run Higress Gateway API Tests
run: |-
GOPROXY="https://proxy.golang.org,direct" make gateway-conformance-test \
TAG="${HIGRESS_TEST_IMAGE_TAG}" \
HIGRESS_CONFORMANCE_VERSION="${HIGRESS_TEST_IMAGE_TAG}"
- name: Collect Gateway API Test Diagnostics
if: always()
run: |-
mkdir -p out/gateway-api-conformance/diagnostics
kubectl get gatewayclass,gateway,httproute,referencegrant -A -o yaml > out/gateway-api-conformance/diagnostics/resources.yaml 2>&1 || true
kubectl get events -A --sort-by=.lastTimestamp > out/gateway-api-conformance/diagnostics/events.txt 2>&1 || true
kubectl logs -n "${GATEWAY_API_TEST_NAMESPACE}" deployment/higress-controller --all-containers > out/gateway-api-conformance/diagnostics/controller.log 2>&1 || true
kubectl logs -n "${GATEWAY_API_TEST_NAMESPACE}" deployment/higress-gateway --all-containers > out/gateway-api-conformance/diagnostics/gateway.log 2>&1 || true
tools/bin/kind-gateway-api export logs out/gateway-api-conformance/diagnostics/kind --name higress || true
- name: Upload Gateway API Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: gateway-conformance-test
path: out/gateway-api-conformance/
if-no-files-found: warn
- name: Clean Gateway API Test Environment
if: always()
run: make gateway-conformance-test-clean
higress-conformance-test:
runs-on: ubuntu-22.04

View File

@@ -0,0 +1,32 @@
name: "Check golang-filter Envoy Sync"
on:
push:
branches: [main]
paths:
- "plugins/golang-filter/go.mod"
- "envoy/envoy"
- ".gitmodules"
- "tools/hack/check-golang-filter-envoy-sync.sh"
- ".github/workflows/check-golang-filter-envoy-sync.yaml"
pull_request:
branches: ["*"]
paths:
- "plugins/golang-filter/go.mod"
- "envoy/envoy"
- ".gitmodules"
- "tools/hack/check-golang-filter-envoy-sync.sh"
- ".github/workflows/check-golang-filter-envoy-sync.yaml"
workflow_dispatch: ~
jobs:
check-envoy-sync:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
# The check reads the submodule commit from the tree, so submodules
# do not need to be fetched.
submodules: false
- name: "Verify golang-filter Envoy pin matches the envoy/envoy submodule"
run: bash tools/hack/check-golang-filter-envoy-sync.sh

View File

@@ -115,7 +115,8 @@ jobs:
run: |
echo "Building WASM for ${{ matrix.plugin }}..."
# 检查是否存在main.go文件
# Run prepare.sh if it exists (e.g., download BPE vocabulary)
if [ -f prepare.sh ]; then sh ./prepare.sh; fi
export GOOS=wasip1
export GOARCH=wasm

3
.gitignore vendored
View File

@@ -17,3 +17,6 @@ target/
tools/hack/cluster.conf
envoy/1.20
istio/1.12
# Local working notes (design specs, implementation plans) — not for upstream PRs
docs/superpowers/

4
.issue-spec/config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"hostname": "github.com",
"repo": "higress-group/higress"
}

View File

@@ -36,7 +36,9 @@ header:
- 'pkg/ingress/kube/gateway/istio/testdata'
- 'release-notes/**'
- '.cursor/**'
- '.claude/**'
- '.claude/**'
- '.agents/**'
- '.issue-spec/**'
comment: on-failure
dependency:

156
AGENTS.md Normal file
View File

@@ -0,0 +1,156 @@
# AGENTS.md
Guidance for AI agents working in this repository.
Higress is a cloud-native API gateway built on Istio and Envoy. The control
plane extends Istio/pilot (Go); the data plane is Envoy extended with WASM
plugins (Go/Rust/C++/AssemblyScript) and a Go-based `golang-filter`. It supports
Ingress/Gateway API and ships a rich plugin ecosystem (including AI gateway
plugins).
## Repository layout
Top-level directories (all paths relative to repo root):
- `cmd/higress/` — main entrypoint (`main.go`) for the Higress controller binary.
- `pkg/` — core Go control-plane packages: `bootstrap/`, `cert/`, `cmd/`,
`common/`, `config/`, `ingress/` (Ingress/Gateway config translation),
`kube/`.
- `api/` — protobuf/CRD API definitions; Higress CRDs live in
`api/extensions/v1alpha1` (e.g. the `WasmPlugin` type). Generated with
`make gen-api` / `make gen-client` (see `api/gen.sh`, `buf.*`).
- `client/` — generated Go clientset for Higress CRDs.
- `istio/` — git submodules of higress-group forks of Istio (`api`, `istio`,
`client-go`, `pkg`, `proxy`); see `.gitmodules`. Pulled via `make submodule`
(part of `prebuild`).
- `envoy/` — Envoy + `go-control-plane` submodules (higress-group forks).
- `external/` — vendored/external mirror dirs used during build (istio, envoy,
proxy, etc.).
- `plugins/` — all data-plane plugins (see "Plugins" below).
- `registry/` — service-discovery registry integrations (nacos, consul, eureka,
zookeeper, direct, mcp, ...).
- `hgctl/` — the `hgctl` CLI (separate Go module) for managing Higress.
- `helm/` — Helm charts: `helm/core` (the dev/install chart) and `helm/higress`.
- `test/``test/e2e/` (conformance/e2e, see "Build & test") and
`test/gateway/`.
- `tools/` — build/CI scripting: `tools/hack/` (build scripts), `tools/bin/`,
`tools/linter/`, `*.mk`.
- `samples/` — example manifests (gateway-api, hello-world, wasmplugin, ...).
- `docker/`, `docs/`, `release-notes/` — packaging, docs, and release notes.
- `Makefile` — istio common-files wrapper (supports `BUILD_WITH_CONTAINER`);
real targets live in `Makefile.core.mk` (+ `Makefile.overrides.mk`).
## Plugins
All plugins live under `plugins/`. See `plugins/README.md` for the contributor
overview. Prebuilt plugin images are published to
`higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins`.
### plugins/wasm-go/ (primary WASM plugin framework, Go)
- `extensions/<name>/` — one directory per plugin (~59 plugins, many `ai-*`).
Each plugin is its own Go module: `main.go`, `go.mod`/`go.sum`, `VERSION`,
`README.md`(+`README_EN.md`), often `config/`, `util/`, `main_test.go`.
Optional `.buildrc` sets `EXTRA_TAGS`; optional `prepare.sh`/`prepare.sh`.
`plugin.wasm` is a build artifact and is **not** committed.
- Shared SDK: plugins depend on external modules
`github.com/higress-group/wasm-go` and
`github.com/higress-group/proxy-wasm-go-sdk` (NOT an in-repo SDK dir).
In-repo, `plugins/wasm-go/pkg/mcp/` provides MCP helpers and
`plugins/wasm-go/mcp-servers/` holds MCP server plugins.
- `examples/` — minimal reference plugins (custom-log, custom-span-attribute,
test-foreign-function).
- Build: `plugins/wasm-go/Makefile`. `PLUGIN_NAME=<name> make build` builds a
wasm file (output to `extensions/<name>/plugin.wasm`) + image via
`Dockerfile`/`DockerfileBuilder` (uses a `wasm-go-builder` image, Go 1.24,
TinyGo optional). `make build-push` pushes the image; `make local-build`
builds locally with `GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared`.
- Conventions: `VERSION` is the image tag; the CI/e2e batch builder
(`tools/hack/build-wasm-plugins.sh`) only compiles a wasm-go plugin whose
`VERSION` ends in `-alpha` (see the section at the bottom of this file).
### plugins/wasm-rust/ (Rust WASM plugins)
- Workspace-style: root `Cargo.toml`/`Cargo.lock`, shared `src/`,
`extensions/<name>/` per plugin (e.g. `ai-data-masking`, `ai-intent`,
`request-block`, `say-hello`, `demo-wasm`), `example/`.
- Build via `plugins/wasm-rust/Makefile` (`PLUGIN_NAME=<name> make build`, plus
`lint`/`test`); the batch builder runs it when `PLUGIN_TYPE=RUST`.
### plugins/wasm-cpp/ (C++ WASM plugins, Bazel)
- Bazel project: `WORKSPACE`, `BUILD`, `bazel/`, `common/`, `scripts/`,
`extensions/<name>/` (e.g. `basic_auth`, `jwt_auth`, `key_rate_limit`,
`model_router`, ...). Build via `plugins/wasm-cpp/Makefile`
(`PLUGIN_NAME=<name> make build`), invoked with `PLUGIN_TYPE=CPP`.
### plugins/wasm-assemblyscript/ (AssemblyScript WASM plugins)
- Node/AssemblyScript project: `asconfig.json`, `package.json`, `assembly/`,
`extensions/`.
### plugins/golang-filter/ (Envoy Go HTTP filter, NOT WASM)
- A native Envoy Golang HTTP filter (`main.go`, `mcp-server/`, `mcp-session/`);
compiled as a shared object (`.so`) independent of Envoy — no Envoy rebuild
needed. Requires Higress >= 2.1.0. Plugins register in `main.go`'s `init()`
via `RegisterHttpFilterFactoryAndConfigParser`. See
`plugins/golang-filter/README.md`.
- Build: `plugins/golang-filter/Makefile` (docker build, outputs
`golang-filter_<arch>.so`). Wired into the gateway image build via
`Makefile.core.mk` targets `build-golang-filter[-amd64|-arm64]`.
### How plugins are loaded
`WasmPlugin` CRDs (`extensions.higress.io/v1alpha1`) reference a plugin by
`url:` — either `oci://.../plugins/<name>:<version>` (image) or
`file:///opt/plugins/.../plugin.wasm` (local mount used in e2e). The dev install
`make install-dev-wasmplugin` sets Helm `global.volumeWasmPlugins=true` to mount
locally built wasm files into the gateway.
## Build & test
Run targets from the repo root; `Makefile` delegates to `Makefile.core.mk`.
Common ones:
- `make build` / `make build-linux` — build the Higress controller binary
(`prebuild` first fetches submodules).
- `make build-hgctl` — build the `hgctl` CLI.
- `make build-gateway` / `make build-istio` / `make build-envoy` — data-plane
and control-plane images (gateway pulls in the golang-filter).
- `make build-wasmplugins` — runs `tools/hack/build-wasm-plugins.sh` to batch
build WASM plugins (respects `PLUGIN_TYPE` / `PLUGIN_NAME`; Go plugins require
a `-alpha` VERSION).
- `make gen-api` / `make gen-client` — regenerate API/client code.
### Conformance / e2e tests (`test/e2e/`)
- Entrypoint `test/e2e/e2e_test.go`, run with build tag `conformance` and
`--test-area` / `--execute-tests` flags.
- Cases live in `test/e2e/conformance/tests/` as **paired `<name>.go` +
`<name>.yaml`** files (~68 cases; WASM cases are prefixed by language, e.g.
`go-wasm-*`, `cpp-wasm-*`). Support code: `conformance/base/`,
`conformance/utils/`, `conformance/embed.go`.
- Key Make targets (each spins up a kind cluster):
- `make higress-conformance-test` — Ingress/Gateway conformance.
- `make higress-wasmplugin-test` — WASM plugin e2e (uses
`install-dev-wasmplugin`, which builds plugins and mounts them).
- `*-prepare` / `*-skip-docker-build` / `*-clean` variants exist for
iterating; `run-higress-e2e-test[-wasmplugin]` runs `go test` against an
already-prepared cluster (filter with `TEST_SHORTNAME`).
- For the specifics of authoring a wasm-go e2e test, see the section below.
## Writing e2e conformance tests with wasm-go plugins
When adding an e2e conformance test that ships its own wasm-go plugin under
`plugins/wasm-go/extensions/<name>/`:
- The plugin's `VERSION` file **must end in `-alpha`** (e.g. `1.0.0-alpha`).
CI's `tools/hack/build-wasm-plugins.sh` only compiles a wasm-go plugin when
its version ends in `-alpha`; otherwise it silently skips it.
- `plugin.wasm` is a build artifact and is **not** committed. If the plugin
isn't built, the `file:///opt/plugins/.../plugin.wasm` URL in the test's
`WasmPlugin` manifest resolves to a missing file, envoy rejects the wasm
config and fails closed, and every request on that route returns HTTP 500.
Locally this can be masked because a previously built `plugin.wasm` still
exists on disk — so a test can pass locally yet 500 in CI.

79
COMMUNITY.md Normal file
View File

@@ -0,0 +1,79 @@
# Higress Community
This document is the authoritative inventory of official Higress project
communication channels, including channels used by its subprojects. Official
technical and governance decisions are recorded in public GitHub issues,
discussions, pull requests, or published meeting notes.
## Public channels
| Channel | Scope and purpose |
| --- | --- |
| [GitHub Issues](https://github.com/higress-group/higress/issues) | Bug reports, feature requests, and work tracking for the primary repository |
| [GitHub Pull Requests](https://github.com/higress-group/higress/pulls) | Public change proposals, reviews, and decision records |
| [GitHub Discussions](https://github.com/higress-group/higress/discussions) | User questions, ideas, announcements, and longer-form community discussion |
| [Discord](https://discord.gg/tSbww9VDaM) | Public real-time user and contributor chat; decisions arising there must be recorded on GitHub |
| [Higress mailing list](mailto:higress@googlegroups.com) | Community questions and contributor contact by email |
| [Chinese-language community group](./README_ZH.md#%E4%BA%A4%E6%B5%81%E7%BE%A4) | Publicly advertised Chinese-language user and contributor chat |
| [Higress WeChat Official Account](./README_ZH.md#%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB) | Chinese-language technical articles and project announcements; broadcast rather than a decision channel |
| [Higress website and documentation](https://higress.cn/en/) | Published user and contributor documentation and project announcements |
Each subproject uses its own public GitHub issues and pull requests for work
specific to that repository:
| Subproject | Issues | Pull requests |
| --- | --- | --- |
| `higress-console` | [Issues](https://github.com/higress-group/higress-console/issues) | [Pull requests](https://github.com/higress-group/higress-console/pulls) |
| `higress-standalone` | [Issues](https://github.com/higress-group/higress-standalone/issues) | [Pull requests](https://github.com/higress-group/higress-standalone/pulls) |
| `plugin-server` | [Issues](https://github.com/higress-group/plugin-server/issues) | [Pull requests](https://github.com/higress-group/plugin-server/pulls) |
| `wasm-go` | [Issues](https://github.com/higress-group/wasm-go/issues) | [Pull requests](https://github.com/higress-group/wasm-go/pulls) |
Cross-subproject and project-governance decisions are recorded in the primary
Higress repository.
## Non-public channels
Non-public channels are limited to reports whose confidentiality protects
reporters or users:
| Channel | Special purpose |
| --- | --- |
| [GitHub Private Security Advisories](https://github.com/higress-group/higress/security/advisories/new) | One of the two required confidential vulnerability-reporting channels; used for project triage, remediation, and disclosure coordination under [`SECURITY.md`](./SECURITY.md) |
| [Alibaba Security Response Center](https://security.alibaba.com/) | The second required confidential vulnerability-reporting channel; the same substantive report must be submitted here and correlated with the GitHub advisory by the Security Response Team |
| [CNCF Code of Conduct reporting](mailto:conduct@cncf.io) | Confidential Code of Conduct incident reporting under [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md) |
| [CNCF TOC private mailing list](mailto:cncf-private-toc@lists.cncf.io) | Confidential escalation when project leadership cannot resolve a security or governance conflict without conflicted participants |
Personal messages, employer-internal systems, and informal maintainer chats are
not official project decision channels. If they inform project work, the
non-sensitive decision and rationale must be recorded publicly.
## Community meetings
Higress does not currently run a recurring public community meeting and does
not yet have a CNCF calendar entry. Establishing an up-to-date public meeting
scheduler and/or CNCF calendar integration is tracked as an Incubation
readiness item in
[`docs/cncf/governance-review.md`](./docs/cncf/governance-review.md).
When a recurring meeting is established, its schedule and joining information
will be published here and on the CNCF calendar. Agendas, notes, recordings,
and decisions will be public, with confidential security and Code of Conduct
matters excluded.
## Contributor activity and recruitment
Public, continuously updated evidence is available through:
- [GitHub contributor activity](https://github.com/higress-group/higress/graphs/contributors)
- [Recent repository activity](https://github.com/higress-group/higress/pulse)
- [CNCF DevStats for Higress](https://higress.devstats.cncf.io/)
- [`help wanted` issues](https://github.com/higress-group/higress/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
- [`good first issue` issues](https://github.com/higress-group/higress/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- the contributor path in [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md)
- the active-maintainer evidence and annual roster review in
[`MAINTAINERS.md`](./MAINTAINERS.md)
Contributors can start through an issue, discussion, documentation update, bug
fix, test, plugin, or other pull request. Maintainers and code owners recruit
and mentor contributors through the public channels above and identify
approachable work with contribution labels.

View File

@@ -19,22 +19,109 @@ Higress governance is guided by the following values:
## Roles
Higress role definitions are documented in:
Higress has the following project-wide roles:
- **Contributor**: anyone who participates through issues, discussions,
documentation, code, testing, or other community work. Contributors do not
need a formal appointment.
- **Code owner**: a contributor delegated to review changes in one or more
paths in [`CODEOWNERS`](./CODEOWNERS). Code owners assess technical quality,
request changes, and help maintain their assigned area. Code ownership does
not grant project-wide governance authority or a permanent seat. A code
owner is added or removed by a public pull request updating `CODEOWNERS`,
using the decision process below. Maintainers remain accountable for the
resulting ownership coverage and may merge only in accordance with the
repository's configured permissions and review rules.
- **Maintainer**: a project-wide leadership role responsible for technical
direction, releases, governance, community health, and the delegation of
code ownership. Maintainers are not limited to the paths where they are
named as code owners.
The current roster, responsibilities, and lifecycle are documented in:
- [`MAINTAINERS.md`](./MAINTAINERS.md)
- [`CODEOWNERS`](./CODEOWNERS)
- [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md)
## Project Scope and Subprojects
The following repositories comprise the CNCF Higress project. Unless a
subproject documents an additional local rule, this governance and the
project-wide maintainer roster apply to it.
| Repository | Responsibility | Status |
| --- | --- | --- |
| [`higress-group/higress`](https://github.com/higress-group/higress) | Core control plane, data plane integration, APIs, Helm charts, CLI, plugins, documentation, and releases | Active; primary repository |
| [`higress-group/higress-console`](https://github.com/higress-group/higress-console) | Web management console | Active subproject |
| [`higress-group/higress-standalone`](https://github.com/higress-group/higress-standalone) | Standalone and local deployment packaging | Active subproject |
| [`higress-group/plugin-server`](https://github.com/higress-group/plugin-server) | Plugin distribution service used by Higress | Active subproject |
| [`higress-group/wasm-go`](https://github.com/higress-group/wasm-go) | Go SDK for Higress Wasm plugins | Active subproject |
Git submodules declared in [`.gitmodules`](./.gitmodules), including the
Higress-hosted forks of Istio, Envoy, and their related API and client
libraries, are pinned source dependencies used to build Higress; they are not
separately governed Higress subprojects. Other upstream dependency forks,
integrations, examples, experiments, websites, and repositories in the
`higress-group` organization are also not Higress subprojects unless they are
added to this table.
A proposal to add, remove, transfer, or archive a subproject must be made in a
public issue or pull request. It must identify the repository, purpose,
maintainers or delegated code owners, contribution path, communication
channels, and lifecycle status. The proposal follows the project-direction
decision process below. An archived or removed subproject remains listed here
with its final status and successor, if any.
## Decision Making
Higress uses **lazy consensus** by default.
When consensus cannot be reached, maintainers may start a vote on a public
issue or pull request. A simple majority of votes cast decides the outcome.
Technical proposals, contribution acceptance, project goals, leadership and
role assignments, subproject changes, requests made on behalf of Higress to
the CNCF, and governance changes are discussed in public issues or pull
requests. Maintainers are responsible for ensuring that significant decisions
record their rationale and outcome in the relevant public thread.
When consensus cannot be reached, a maintainer may start a vote on the public
issue or pull request. A simple majority of non-conflicted maintainer votes
cast decides the outcome. A maintainer with a material personal or employer
conflict must disclose it and abstain from the decision. If all maintainers are
conflicted, the project will request guidance from the CNCF TOC.
For governance or project direction changes, maintainers should allow adequate
time for public discussion before finalizing decisions.
## Vendor Neutrality
Higress project direction and governance are independent of any single
company. Maintainer and code-owner roles are held by individuals, not reserved
for employers. No employer has a guaranteed seat, veto, or preferred decision
weight. Contributions, integrations, defaults, and roadmap priorities are
evaluated on community and technical merit. Company-specific products may be
supported, but they do not receive privileged governance treatment.
Affiliation changes do not by themselves add or remove a role. Maintainers
disclose affiliations in `MAINTAINERS.md` and disclose material conflicts in
the public decision record.
## Function-Based Teams
Maintainers may create a function-based team, such as a release or security
team, through a public governance pull request. The change must document the
team's purpose, authority, membership or selection method, onboarding,
conflict handling, removal, and retirement. Sensitive operational details may
remain private, but the team's mandate and decision authority must be public.
The current Security Response Team and its private report-handling process are
defined in [`SECURITY.md`](./SECURITY.md).
## Community and Meetings
The authoritative inventory of public and non-public communication channels,
subproject channels, contribution activity, and meeting information is in
[`COMMUNITY.md`](./COMMUNITY.md). Project decisions must be recorded in a
public issue, discussion, pull request, or published meeting notes even when
preliminary conversation occurred elsewhere.
## Governance Updates
Changes to this document are made through pull requests and approved by

View File

@@ -2,24 +2,49 @@
This file lists the current maintainers of the Higress project.
A maintainer is a contributor who is actively responsible for guiding the
direction of the project, reviewing and merging contributions, and
participating in project governance.
A maintainer is a contributor who is actively responsible for project-wide
technical direction, releases, governance, community health, and the
delegation of code ownership across the Higress project and its subprojects.
Maintainer authority is held by individuals and is not tied to an employer.
For the day-to-day code-review ownership of individual subdirectories,
see [`CODEOWNERS`](./CODEOWNERS).
## Maintainers
| Name | GitHub | Affiliation |
| -------------- | ------------------------------------------------------- | ----------------- |
| Yiquan Dong | [@CH3CHO](https://github.com/CH3CHO) | Trip.com |
| Yuanxiao Zhao | [@EndlessSeeker](https://github.com/EndlessSeeker) | Alibaba Cloud |
| Leilei Geng | [@gengleilei](https://github.com/gengleilei) | Alibaba Cloud |
| Xiantao Han | [@hanxiantao](https://github.com/hanxiantao) | XinYe Technology |
| Zhiwei Cheng | [@cr7258](https://github.com/cr7258) | NVIDIA |
| Tianyi Zhang | [@johnlanni](https://github.com/johnlanni) | Alibaba Cloud |
| Jingfeng Xu | [@lexburner](https://github.com/lexburner) | Alibaba Cloud |
| Name | GitHub contact | Domain of responsibility | Affiliation |
| --- | --- | --- | --- |
| Yiquan Dong | [@CH3CHO](https://github.com/CH3CHO) | Project-wide governance, technical direction, releases, and community stewardship | Trip.com |
| Yuanxiao Zhao | [@EndlessSeeker](https://github.com/EndlessSeeker) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
| Leilei Geng | [@gengleilei](https://github.com/gengleilei) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
| Xiantao Han | [@hanxiantao](https://github.com/hanxiantao) | Project-wide governance, technical direction, releases, and community stewardship | XinYe Technology |
| Zhiwei Cheng | [@cr7258](https://github.com/cr7258) | Project-wide governance, technical direction, releases, and community stewardship | NVIDIA |
| Tianyi Zhang | [@johnlanni](https://github.com/johnlanni) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
| Jingfeng Xu | [@lexburner](https://github.com/lexburner) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
More specific day-to-day review responsibilities are delegated through
[`CODEOWNERS`](./CODEOWNERS); they do not narrow a maintainer's project-wide
responsibility.
## Activity and roster review
A maintainer is active when they have participated in project review,
development, releases, issue triage, community support, or governance during
the preceding 12 months and remain willing to perform the role. The roster is
reviewed in a public pull request at least annually. The review checks public
activity, current affiliation and contact information, and asks maintainers
whose status is unclear to confirm their availability.
The 2026 roster review used public GitHub issue and pull-request activity for
each maintainer:
- [Yiquan Dong](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3ACH3CHO)
- [Yuanxiao Zhao](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3AEndlessSeeker)
- [Leilei Geng](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Agengleilei)
- [Xiantao Han](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Ahanxiantao)
- [Zhiwei Cheng](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Acr7258)
- [Tianyi Zhang](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Ajohnlanni)
- [Jingfeng Xu](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Alexburner)
## Becoming a maintainer
@@ -28,7 +53,29 @@ Higress follows the contribution and graduation paths described in
contributors who consistently demonstrate good technical judgement and
community stewardship can be nominated as maintainers by an existing
maintainer; nominations are accepted by lazy consensus among the current
maintainers.
maintainers. The nomination pull request must update this roster and state the
candidate's public contribution history, domain of responsibility, contact,
and affiliation.
## Leaving or changing maintainer status
- A maintainer may step down by opening or approving a pull request that moves
them to the emeritus list or removes them from the roster.
- A maintainer who has no qualifying activity for 12 months will be contacted
publicly where practical and given at least 30 days to confirm whether they
want to resume the role, move to emeritus, or step down. If there is no
response, another maintainer may propose the change through lazy consensus.
- A maintainer may be removed for sustained failure to perform the role or for
a Code of Conduct violation. The proposal follows the governance decision
process, excludes conflicted maintainers, and preserves confidential details
where required by the Code of Conduct process.
- Emeritus maintainers retain recognition but no maintainer authority or
required repository access. An emeritus or former maintainer may return
through the same public nomination process used for a new maintainer.
## Emeritus maintainers
There are currently no emeritus maintainers.
## Reporting issues

View File

@@ -21,6 +21,21 @@ GO ?= go
export GOPROXY ?= https://proxy.golang.org,direct
GATEWAY_API_VERSION ?= v1.4.0
GATEWAY_CONFORMANCE_PROFILE ?= GATEWAY-HTTP
GATEWAY_CONFORMANCE_SUPPORTED_FEATURES ?= Gateway,HTTPRoute,ReferenceGrant
GATEWAY_CONFORMANCE_REPORT ?= out/gateway-api-conformance/report.yaml
GATEWAY_CONFORMANCE_CONTACT ?= https://github.com/alibaba/higress/issues
GATEWAY_CONFORMANCE_RUN_TEST ?=
GATEWAY_CONFORMANCE_ALLOW_CRDS_MISMATCH ?= false
GATEWAY_API_TEST_NAMESPACE ?= gateway-conformance-infra
GATEWAY_API_GATEWAY_SERVICE_TYPE ?= ClusterIP
GATEWAY_API_DIAL_LOCALHOST ?= true
GATEWAY_API_LOCAL_HTTP_PORT ?= 80
GATEWAY_API_LOCAL_HTTPS_PORT ?= 443
GATEWAY_API_KIND_NODE_TAG ?= v1.34.0@sha256:7416a61b42b1662ca6ca89f02028ac133a309a2a30ba309614e8ec94d976dc5a
HIGRESS_CONFORMANCE_VERSION ?= $(shell git rev-parse HEAD)
TARGET_ARCH ?= amd64
VALID_ARCHS := amd64 arm64
@@ -151,7 +166,7 @@ docker-buildx-push: clean-env docker.higress-buildx
export PARENT_GIT_TAG:=$(shell cat VERSION)
export PARENT_GIT_REVISION:=$(TAG)
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.2.1/envoy-symbol-ARCH.tar.gz
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.2.4-rc.2-test-cpp-host/envoy-symbol-ARCH.tar.gz
build-envoy: prebuild
./tools/hack/build-envoy.sh
@@ -205,11 +220,15 @@ install: pre-install
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
HIGRESS_LATEST_IMAGE_TAG ?= latest
ENVOY_LATEST_IMAGE_TAG ?= 36c1d07376bf11295edc40357d74a5ecb50122b1
ISTIO_LATEST_IMAGE_TAG ?= 36c1d07376bf11295edc40357d74a5ecb50122b1
ENVOY_LATEST_IMAGE_TAG ?= 481184afc44176eb23d64e0011dc3ea1ae6a410c
ISTIO_LATEST_IMAGE_TAG ?= de2c9628294f51b13c4a70b3a862b4372890797a
install-dev: pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
install-dev-gateway-api: pre-install
helm install higress helm/core -n $(GATEWAY_API_TEST_NAMESPACE) --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true' --set 'gateway.service.type=$(GATEWAY_API_GATEWAY_SERVICE_TYPE)'
install-dev-wasmplugin: build-wasmplugins pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true' --set 'global.volumeWasmPlugins=true' --set 'global.onlyPushRouteCluster=false'
@@ -262,9 +281,71 @@ clean: clean-higress clean-gateway clean-istio clean-env clean-tool
include tools/tools.mk
include tools/lint.mk
# gateway-conformance-test runs gateway api conformance tests.
# install-gateway-api-crds installs the Gateway API CRDs used by the conformance suite.
.PHONY: install-gateway-api-crds
install-gateway-api-crds:
kubectl apply --server-side=true -f https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEWAY_API_VERSION)/standard-install.yaml
kubectl wait --for=condition=Established crd/gatewayclasses.gateway.networking.k8s.io --timeout=120s
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=120s
kubectl wait --for=condition=Established crd/httproutes.gateway.networking.k8s.io --timeout=120s
kubectl wait --for=condition=Established crd/referencegrants.gateway.networking.k8s.io --timeout=120s
# create-gateway-api-cluster creates the Kubernetes version used by Gateway API v1.4.0 tests.
.PHONY: create-gateway-api-cluster
create-gateway-api-cluster: $(tools/kind-gateway-api)
KIND=$(tools/kind-gateway-api) KIND_NODE_TAG=$(GATEWAY_API_KIND_NODE_TAG) tools/hack/create-cluster.sh
# delete-gateway-api-cluster deletes the Gateway API test cluster.
.PHONY: delete-gateway-api-cluster
delete-gateway-api-cluster: $(tools/kind-gateway-api)
$(tools/kind-gateway-api) delete cluster --name higress
# kube-load-gateway-api-images loads only the images required by the Gateway API tests.
.PHONY: kube-load-gateway-api-images
kube-load-gateway-api-images: $(tools/kind-gateway-api)
KIND=$(tools/kind-gateway-api) tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG)
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot $(ISTIO_LATEST_IMAGE_TAG)
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway $(ENVOY_LATEST_IMAGE_TAG)
KIND=$(tools/kind-gateway-api) tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot $(ISTIO_LATEST_IMAGE_TAG)
KIND=$(tools/kind-gateway-api) tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway $(ENVOY_LATEST_IMAGE_TAG)
# gateway-conformance-test-prepare prepares a kind cluster for Gateway API tests.
.PHONY: gateway-conformance-test-prepare
gateway-conformance-test-prepare: delete-gateway-api-cluster create-gateway-api-cluster install-gateway-api-crds docker-build kube-load-gateway-api-images install-dev-gateway-api
kubectl wait --timeout=10m -n $(GATEWAY_API_TEST_NAMESPACE) deployment/higress-controller --for=condition=Available
kubectl wait --timeout=10m -n $(GATEWAY_API_TEST_NAMESPACE) deployment/higress-gateway --for=condition=Available
kubectl wait --timeout=10m gatewayclass/higress --for=condition=Accepted
# run-gateway-conformance-test runs the upstream Gateway API Conformance Suite.
.PHONY: run-gateway-conformance-test
run-gateway-conformance-test:
mkdir -p $(dir $(GATEWAY_CONFORMANCE_REPORT))
HIGRESS_GATEWAY_API_TEST_DIAL_LOCALHOST=$(GATEWAY_API_DIAL_LOCALHOST) \
HIGRESS_GATEWAY_API_TEST_LOCAL_HTTP_PORT=$(GATEWAY_API_LOCAL_HTTP_PORT) \
HIGRESS_GATEWAY_API_TEST_LOCAL_HTTPS_PORT=$(GATEWAY_API_LOCAL_HTTPS_PORT) go test -v ./test/gateway \
-run '^TestGatewayAPIConformance$$' \
-args \
--gateway-class=higress \
--supported-features=$(GATEWAY_CONFORMANCE_SUPPORTED_FEATURES) \
--conformance-profiles=$(GATEWAY_CONFORMANCE_PROFILE) \
--organization=alibaba \
--project=higress \
--url=https://github.com/alibaba/higress \
--version=$(HIGRESS_CONFORMANCE_VERSION) \
--contact=$(GATEWAY_CONFORMANCE_CONTACT) \
--mode=default \
--cleanup-base-resources=false \
--allow-crds-mismatch=$(GATEWAY_CONFORMANCE_ALLOW_CRDS_MISMATCH) \
$(if $(GATEWAY_CONFORMANCE_RUN_TEST),--run-test=$(GATEWAY_CONFORMANCE_RUN_TEST),) \
--report-output=$(abspath $(GATEWAY_CONFORMANCE_REPORT))
# gateway-conformance-test runs Gateway API tests as a standard Higress integration test.
.PHONY: gateway-conformance-test
gateway-conformance-test:
gateway-conformance-test: gateway-conformance-test-prepare run-gateway-conformance-test
# gateway-conformance-test-clean deletes the Gateway API test cluster.
.PHONY: gateway-conformance-test-clean
gateway-conformance-test-clean: delete-gateway-api-cluster
# higress-conformance-test-prepare prepares the environment for higress conformance tests.
.PHONY: higress-conformance-test-prepare

View File

@@ -8,8 +8,8 @@
<div align="center">
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![Build Status](https://github.com/higress-group/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/higress-group/higress/actions)
[![license](https://img.shields.io/github/license/higress-group/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![discord](https://img.shields.io/discord/1364956090566971515?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/tSbww9VDaM)
[![CNCF Sandbox](https://img.shields.io/badge/CNCF-Sandbox-30638E?logo=linuxfoundation&logoColor=white)](https://www.cncf.io/projects/)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12667/badge)](https://www.bestpractices.dev/projects/12667)
@@ -21,6 +21,7 @@
[**Official Site**](https://higress.ai/en/) &nbsp; |
&nbsp; [**Docs**](https://higress.cn/en/docs/latest/overview/what-is-higress/) &nbsp; |
&nbsp; [**Blog**](https://higress.cn/en/blog/) &nbsp; |
&nbsp; [**Roadmap**](./ROADMAP.md) &nbsp; |
&nbsp; [**MCP Server QuickStart**](https://higress.cn/en/ai/mcp-quick-start/) &nbsp; |
&nbsp; [**Developer Guide**](https://higress.cn/en/docs/latest/dev/architecture/) &nbsp; |
&nbsp; [**Wasm Plugin Hub**](https://higress.cn/en/plugin/) &nbsp; |
@@ -35,19 +36,19 @@ Higress is a cloud-native API gateway based on Istio and Envoy, which can be ext
### Core Use Cases
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international. It also supports hosting MCP (Model Context Protocol) Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers for hosting. Higress provides unified management for both LLM API and MCP API.
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/higress-group/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international. It also supports hosting MCP (Model Context Protocol) Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers for hosting. Higress provides unified management for both LLM API and MCP API.
**🌟 Try it now at [https://mcp.higress.ai/](https://mcp.higress.ai/)** to experience Higress-hosted Remote MCP Servers firsthand:
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### Enterprise Adoption
### Production Adoption
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo. Within Alibaba Cloud, Higress's AI gateway capabilities support core AI applications such as Tongyi Bailian model studio, machine learning PAI platform, and other critical AI services. Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
You can click the button below to install the enterprise version of Higress:
[![Deploy on AlibabaCloud](https://img.alicdn.com/imgextra/i1/O1CN01e6vwe71EWTHoZEcpK_!!6000000000359-55-tps-170-40.svg)](https://www.aliyun.com/product/api-gateway?spm=higress-github.topbar.0.0.0)
Higress originated at Alibaba to address long-connection disruption during
gateway reloads and improve gRPC/Dubbo load balancing. It is now developed as
a vendor-neutral CNCF project and is used by organizations across multiple
industries. Public adopters and their use cases are listed in
[`ADOPTERS.md`](./ADOPTERS.md).
## Summary
@@ -77,8 +78,9 @@ Port descriptions:
- Port 8080: Gateway HTTP protocol entry
- Port 8443: Gateway HTTPS protocol entry
> All Higress Docker images use Higress's own image repository and are not affected by Docker Hub rate limits.
> In addition, the submission and updates of the images are protected by a security scanning mechanism (powered by Alibaba Cloud ACR), making them very secure for use in production environments.
> Higress publishes project images through dedicated regional registry
> endpoints. Operators may mirror the images to a registry they control and
> configure the Helm `global.hub` value accordingly.
>
> If you experience a timeout when pulling image from `higress-registry.cn-hangzhou.cr.aliyuncs.com`, you can try replacing it with the following docker registry mirror source:
>
@@ -100,9 +102,6 @@ Port descriptions:
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.ai/en/docs/latest/user/quickstart/).
If you are deploying on the cloud, it is recommended to use the [Enterprise Edition](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
## Use Cases
- **MCP Server Hosting**:
@@ -185,6 +184,10 @@ Join our Discord community! This is where you can connect with developers and ot
[![discord](https://img.shields.io/discord/1364956090566971515?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge)](https://discord.gg/tSbww9VDaM)
The complete inventory of public and private communication channels,
subproject channels, meeting information, and contributor activity is in
[`COMMUNITY.md`](./COMMUNITY.md).
### Code of Conduct
The Higress community follows the
@@ -198,7 +201,9 @@ participating in the community.
Project governance, the maintainer roster, and the contribution model are
described in [`GOVERNANCE.md`](./GOVERNANCE.md) and
[`MAINTAINERS.md`](./MAINTAINERS.md). New contributors are encouraged to start
with [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md).
with [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md). Forward planning and release
procedures are documented in [`ROADMAP.md`](./ROADMAP.md) and
[`RELEASE.md`](./RELEASE.md).
### Security
@@ -218,13 +223,13 @@ Higress would not be possible without the valuable open-source work of projects
### Contributors
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
<a href="https://github.com/higress-group/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=higress-group/higress"/>
</a>
### Star History
[![Star History Chart](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=higress-group/higress&type=Date)](https://star-history.com/#higress-group/higress&Date)
---

View File

@@ -6,8 +6,8 @@
</h1>
<h4 align="center"> AIネイティブAPIゲートウェイ </h4>
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![Build Status](https://github.com/higress-group/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/higress-group/higress/actions)
[![license](https://img.shields.io/github/license/higress-group/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![CNCF Sandbox](https://img.shields.io/badge/CNCF-Sandbox-30638E?logo=linuxfoundation&logoColor=white)](https://www.cncf.io/projects/)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12667/badge)](https://www.bestpractices.dev/projects/12667)
@@ -30,15 +30,19 @@ Higressは、IstioとEnvoyをベースにしたクラウドネイティブAPIゲ
### 主な使用シナリオ
HigressのAIゲートウェイ機能は、国内外のすべての[主要モデルプロバイダー](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)をサポートし、vllm/ollamaなどに基づく自己構築DeepSeekモデルにも対応しています。また、プラグインメカニズムを通じてMCPModel Context Protocolサーバーをホストすることもでき、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。[openapi-to-mcpツール](https://github.com/higress-group/openapi-to-mcpserver)を使用すると、OpenAPI仕様を迅速にリモートMCPサーバーに変換してホスティングできます。HigressはLLM APIとMCP APIの統一管理を提供します。
HigressのAIゲートウェイ機能は、国内外のすべての[主要モデルプロバイダー](https://github.com/higress-group/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)をサポートし、vllm/ollamaなどに基づく自己構築DeepSeekモデルにも対応しています。また、プラグインメカニズムを通じてMCPModel Context Protocolサーバーをホストすることもでき、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。[openapi-to-mcpツール](https://github.com/higress-group/openapi-to-mcpserver)を使用すると、OpenAPI仕様を迅速にリモートMCPサーバーに変換してホスティングできます。HigressはLLM APIとMCP APIの統一管理を提供します。
**🌟 今すぐ[https://mcp.higress.ai/](https://mcp.higress.ai/)で体験**してください。HigressがホストするリモートMCPサーバーを直接体験できます:
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### 企業での採用
### 本番環境での採用
Higressは、Tengineのリロード長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。Alibaba Cloud内では、HigressのAIゲートウェイ機能がTongyi Qianwen APP、Tongyi Bailian Model Studio、機械学習PAIプラットフォームなどの中核的なAIアプリケーションをサポートしています。また、国内の主要なAIGC企業ZeroOneやAI製品FastGPTにもサービスを提供しています。Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
Higressは、ゲートウェイのリロードによる長時間接続への影響と
gRPC/Dubboの負荷分散上の課題を解決するため、Alibabaで誕生しました。
現在はベンダーニュートラルなCNCFプロジェクトとしてコミュニティにより
開発され、複数業界の組織で利用されています。公開されている採用組織と
ユースケースは[`ADOPTERS.md`](./ADOPTERS.md)を参照してください。
## 目次
@@ -232,13 +236,13 @@ WeChat公式アカウント
### 貢献者
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
<a href="https://github.com/higress-group/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=higress-group/higress"/>
</a>
### スターの歴史
[![スターの歴史チャート](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
[![スターの歴史チャート](https://api.star-history.com/svg?repos=higress-group/higress&type=Date)](https://star-history.com/#higress-group/higress&Date)
---

View File

@@ -8,8 +8,8 @@
<div align="center">
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![Build Status](https://github.com/higress-group/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/higress-group/higress/actions)
[![license](https://img.shields.io/github/license/higress-group/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![CNCF Sandbox](https://img.shields.io/badge/CNCF-Sandbox-30638E?logo=linuxfoundation&logoColor=white)](https://www.cncf.io/projects/)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12667/badge)](https://www.bestpractices.dev/projects/12667)
@@ -37,7 +37,7 @@ Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy可以用
### 核心使用场景
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型。同时Higress 支持通过插件方式托管 MCP (Model Context Protocol) 服务器,使 AI Agent 能够更容易地调用各种工具和服务。借助 [openapi-to-mcp 工具](https://github.com/higress-group/openapi-to-mcpserver),您可以快速将 OpenAPI 规范转换为远程 MCP 服务器进行托管。Higress 提供了对 LLM API 和 MCP API 的统一管理。
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/higress-group/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型。同时Higress 支持通过插件方式托管 MCP (Model Context Protocol) 服务器,使 AI Agent 能够更容易地调用各种工具和服务。借助 [openapi-to-mcp 工具](https://github.com/higress-group/openapi-to-mcpserver),您可以快速将 OpenAPI 规范转换为远程 MCP 服务器进行托管。Higress 提供了对 LLM API 和 MCP API 的统一管理。
**🌟 立即体验 [https://mcp.higress.ai/](https://mcp.higress.ai/)** 基于 Higress 托管的远程 MCP 服务器:
@@ -45,13 +45,10 @@ Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://
### 生产环境采用
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。在阿里云内部Higress 的 AI 网关能力支撑了通义千问 APP、通义百炼模型工作室、机器学习 PAI 平台等核心 AI 应用。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT。阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
可以点下方按钮安装企业版 Higress:
[![Deploy on AlibabaCloud](https://img.alicdn.com/imgextra/i4/O1CN01tHRaNm22hflDqxKV5_!!6000000007152-55-tps-170-40.svg)](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
如果您使用开源的Higress并希望获得企业级支持可以联系johnlanni的邮箱zty98751@alibaba-inc.com或社交媒体账号微信号nomadao钉钉号chengtanzty。添加好友时请备注Higress :
Higress 最初在阿里内部为解决网关 reload 对长连接业务有损,以及
gRPC/Dubbo 负载均衡能力不足而诞生。Higress 目前作为厂商中立的 CNCF
项目由社区共同开发,并已被多个行业的组织采用。公开采用者和使用场景见
[`ADOPTERS.md`](./ADOPTERS.md)。
## Summary
@@ -80,7 +77,8 @@ docker run -d --rm --name higress-ai -v ${PWD}:/data \
- 8080 端口:网关 HTTP 协议入口
- 8443 端口:网关 HTTPS 协议入口
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
**Higress 通过多个区域的项目镜像仓库发布镜像。使用者也可以将镜像同步到
自己控制的仓库,并通过 Helm 的 `global.hub` 参数进行配置。**
> 如果从 `higress-registry.cn-hangzhou.cr.aliyuncs.com` 拉取镜像超时,可以尝试使用以下镜像加速源:
>
@@ -102,8 +100,6 @@ docker run -d --rm --name higress-ai -v ${PWD}:/data \
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
如果您是在云上部署,推荐使用[企业版](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
## 使用场景
- **AI 网关**:
@@ -261,13 +257,13 @@ Higress 社区遵循 [**CNCF Code of Conduct**](https://github.com/cncf/foundati
### 贡献者
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
<a href="https://github.com/higress-group/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=higress-group/higress"/>
</a>
### Star History
[![Star History](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
[![Star History](https://api.star-history.com/svg?repos=higress-group/higress&type=Date)](https://star-history.com/#higress-group/higress&Date)
---

112
RELEASE.md Normal file
View File

@@ -0,0 +1,112 @@
# Higress Release Process
This document defines the project-wide process for producing a Higress core
release from the primary repository. Subprojects may add repository-specific
steps, but must use the same public approval and security principles.
## Versioning and support
Higress uses [Semantic Versioning](https://semver.org/) with Git tags in the
form `vMAJOR.MINOR.PATCH`. A release-candidate tag may add an `-rc.N` suffix.
- A **major** release may contain incompatible changes and requires migration
guidance.
- A **minor** release adds backward-compatible features and may contain
announced deprecations.
- A **patch** release contains backward-compatible fixes, including security
fixes when appropriate.
The supported major-version lines are listed in [`SECURITY.md`](./SECURITY.md).
A proposal to end support for a major line is made publicly through a pull
request updating `SECURITY.md`, normally at least 90 days before the stated end
of support. Already unsupported versions do not receive routine fixes.
## Roles and authorization
A maintainer acts as **release manager** for each release. The release manager
coordinates the tracking issue, version changes, validation, tag, automation,
and announcement. At least one other unconflicted maintainer reviews and
approves the release pull request. Only maintainers with the required GitHub
and protected-environment access may create release tags or approve publishing
jobs.
Security releases additionally follow [`SECURITY.md`](./SECURITY.md). Embargoed
details stay in the private advisory until the Security Response Team approves
disclosure.
## 1. Plan the release
The release manager opens or identifies a public tracking issue for a normal
release. It records:
- the target version and release type;
- intended scope and linked changes;
- the release manager and expected timing;
- known compatibility, migration, deprecation, and security considerations;
- validation status and unresolved blockers.
An embargoed security release uses a private advisory for planning until public
disclosure is safe.
## 2. Prepare the release pull request
Create a release pull request against `main` that keeps the version in
`VERSION`, `helm/core/Chart.yaml`, `helm/higress/Chart.yaml`, dependency
metadata, and release notes consistent. Update documentation, migration or
deprecation guidance, and supported-version information when they change.
The pull request must pass the repository's required build, unit, race,
conformance, plugin, license, and other configured checks relevant to its
changes. The release manager records any intentionally inapplicable check or
accepted exception in the pull request. A known release-blocking regression,
unresolved critical vulnerability, inconsistent version, or missing migration
guidance blocks the release.
The approving maintainer verifies that the release scope matches the public
plan, required checks passed, versioned dependencies are intentional, and the
release notes accurately describe user-visible changes.
## 3. Tag and publish
After the release pull request is merged, the release manager creates the
corresponding immutable `vMAJOR.MINOR.PATCH` or release-candidate tag from the
reviewed commit. Published tags are not moved or reused.
The tag triggers GitHub Actions that publish, as applicable:
- controller, pilot, and gateway container images;
- Helm charts and chart indexes;
- `hgctl` archives for supported platforms;
- generated CRDs;
- standalone distribution artifacts;
- GitHub release notes and repository release notes.
Manual workflow dispatch is for recovery or a documented exceptional case; it
does not bypass release approval or change the commit being released.
## 4. Verify and announce
The release manager verifies that publishing workflows succeeded and that the
release page, checksums or archives, image tags, Helm charts, CRDs, and release
notes refer to the intended version. Installation and representative gateway
traffic are smoke-tested with the published artifacts before the release is
announced as ready.
Release announcements link the GitHub release and call out breaking changes,
deprecations, migrations, known issues, and security impact. Confirmed security
issues are disclosed through a GitHub Security Advisory and CVE when
appropriate.
## Failure, rollback, and correction
If validation or publishing fails, stop promotion and document the failure in
the tracking issue or release pull request. Do not move or overwrite an
existing public tag. Fix the problem through normal review and publish a new
release-candidate or patch version.
An operator rollback uses the previously pinned Helm chart, configuration, and
images, normally through `helm rollback`. Because CRD schemas and stored
resources may not be reversible, release notes must identify migrations that
need special rollback handling. The project may withdraw a compromised
artifact from distribution, but it preserves a public advisory and audit trail
explaining the affected version and safe replacement.

30
ROADMAP.md Normal file
View File

@@ -0,0 +1,30 @@
# Higress Roadmap
The maintained public Higress roadmap is published on the
[Higress website](https://higress.ai/en/docs/latest/overview/roadmap/). Its
source is version controlled in the
[`higress-group/higress-group.github.io`](https://github.com/higress-group/higress-group.github.io/blob/main/src/content/docs/latest/en/overview/roadmap.md)
repository.
Roadmap entries describe intended direction and target timing, not a guarantee
that a feature or release will ship on a particular date. Security, quality,
compatibility, and contributor availability can change priorities.
## Changing the roadmap
Anyone may propose a roadmap item or priority change through a public
[Higress issue](https://github.com/higress-group/higress/issues) or
[discussion](https://github.com/higress-group/higress/discussions). A proposal
should describe the user problem, expected outcome, project scope, major
dependencies, compatibility or security impact, and a responsible contributor
or maintainer when known.
Maintainers decide roadmap changes through the public lazy-consensus process in
[`GOVERNANCE.md`](./GOVERNANCE.md). Accepted changes are reflected in the
version-controlled website roadmap. Significant changes should link back to
the public issue, discussion, or pull request that records their rationale.
The roadmap is reviewed when planning a minor or major release and whenever a
material project-direction change is accepted. Completed work is documented in
[GitHub releases](https://github.com/higress-group/higress/releases) and the
[`release-notes`](./release-notes) directory.

View File

@@ -17,11 +17,15 @@ your contributions.
**Please do NOT report security vulnerabilities through public GitHub issues,
discussions, or pull requests.**
Instead, please report them through **both** of the following private channels:
Every vulnerability report must be submitted through both of the following
private channels. Please provide the same substantive report to each channel
and, when available, include the case identifier from the other channel so the
Security Response Team can correlate the records. Do not wait for one channel
to acknowledge the report before submitting it to the other.
1. **GitHub Private Security Advisory**:
1. **GitHub Private Security Advisory (required)**:
<https://github.com/higress-group/higress/security/advisories/new>
2. **Alibaba Security Response Center (ASRC)**:
2. **Alibaba Security Response Center (required)**:
<https://security.alibaba.com/>
Please include as much of the following information as possible to help us
@@ -53,9 +57,58 @@ keep you informed of our progress throughout the process.
## Security Response Team
The Higress security response is handled by the project maintainers listed in
[`MAINTAINERS.md`](./MAINTAINERS.md). Security reports submitted via GitHub
Private Security Advisory are visible to all current maintainers.
The Security Response Team (SRT) is composed of the current project
maintainers:
- Yiquan Dong ([@CH3CHO](https://github.com/CH3CHO))
- Yuanxiao Zhao ([@EndlessSeeker](https://github.com/EndlessSeeker))
- Leilei Geng ([@gengleilei](https://github.com/gengleilei))
- Xiantao Han ([@hanxiantao](https://github.com/hanxiantao))
- Zhiwei Cheng ([@cr7258](https://github.com/cr7258))
- Tianyi Zhang ([@johnlanni](https://github.com/johnlanni))
- Jingfeng Xu ([@lexburner](https://github.com/lexburner))
[`MAINTAINERS.md`](./MAINTAINERS.md) is authoritative for membership. A merged
change to that roster onboards or offboards the same person from the SRT and
their private security access must be updated promptly.
For each report, the SRT assigns the following responsibilities in the private
advisory or equivalent confidential case record:
- **Triage coordinator**: acknowledges the report, maintains contact with the
reporter, assigns severity, tracks deadlines, and coordinates the team.
- **Fix lead**: reproduces the issue and develops or coordinates remediation.
- **Reviewer and release lead**: independently reviews the fix, prepares the
supported-version releases, and verifies that artifacts are available.
- **Disclosure lead**: prepares the advisory, CVE request when appropriate,
credits, and coordinated public communication.
One person may perform more than one role, but every confirmed vulnerability
must involve at least two unconflicted SRT members so that remediation receives
independent review.
### Report handling, conflicts, and escalation
1. The SRT correlates the required GitHub Private Security Advisory and Alibaba
Security Response Center submissions. The GitHub advisory or an equivalent
access-controlled project record contains the report, assignments,
decisions, timeline, fix, and disclosure plan; material status and
disclosure updates are reflected in both required reporting records.
2. An SRT member with a personal, employer, or product conflict must disclose
it privately and recuse from severity, release, or disclosure decisions for
that case. The triage coordinator assigns an unconflicted replacement.
3. If acknowledgement, triage, remediation, or disclosure is at risk of
missing the timelines in this policy, the triage coordinator escalates the
case to the full unconflicted SRT and records a revised plan. Critical
vulnerabilities are escalated immediately.
4. If a reporter receives no acknowledgement within three business days, they
should follow up through both private reporting channels and reference both
case identifiers when available. No vulnerability details should be posted
publicly.
5. If fewer than two SRT members are unconflicted, the unconflicted member
escalates confidentially to the CNCF TOC private mailing list at
[cncf-private-toc@lists.cncf.io](mailto:cncf-private-toc@lists.cncf.io)
before a release or disclosure decision is made.
## Disclosure Policy

View File

@@ -1 +1 @@
v2.2.1
v2.2.3

View File

@@ -6,7 +6,7 @@ ARG BASE_VERSION=latest
ARG HUB=higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
ARG TARGETARCH=amd64
ARG TARGETARCH
# The following section is used as base image if BASE_DISTRIBUTION=debug
# This base image is provided by istio, see: https://github.com/istio/istio/blob/master/docker/Dockerfile.base

View File

@@ -0,0 +1,409 @@
# General Technical Review — Higress / Incubation
- **Project:** Higress
- **Project version:** v2.2.3
- **Website:** <https://higress.ai/en/>
- **Date updated:** 2026-07-21
- **Template version:** CNCF General Technical Review v1.0
- **Review state:** Project working draft; maintainer approval and CNCF Project
Reviews verification pending
- **Evidence branch:**
[`higress-group/higress@main`](https://github.com/higress-group/higress/tree/main)
- **Intended TOC snapshot:** `projects/higress/tech-review/YYYY-MM-DD.md`
- **Description:** Higress is a cloud-native API gateway built on Envoy and
Istio. It supports Kubernetes Ingress and Gateway API and is extensible with
Wasm plugins and native Go filters.
This document answers all Day 0 and Day 1 questions required for an Incubation
review. It follows the current CNCF TOC
[General Technical Review questionnaire](https://github.com/cncf/toc/blob/main/toc_subprojects/project-reviews-subproject/general-technical-questions.md).
Statements that are not yet backed by a project guarantee are identified as
gaps rather than treated as completed controls.
This is the project-maintained working copy. For formal Due Diligence, a CNCF
reviewer or associate verifies it and archives a dated snapshot in `cncf/toc`.
The reviewer should freeze evidence links to the reviewed revision when that
snapshot is archived.
## Project Self-Assessment Summary
Higress has substantive evidence for every Day 0 and Day 1 question, but this
document does not assign the project an external technical-review rating. The
strongest Day 1 gaps are the absence of an automated
upgrade→downgrade→upgrade matrix, a published compatibility-review cadence,
and complete release supply-chain attestations. Broad control-plane RBAC also
requires minimization and documented justification. These findings should be
tracked during Due Diligence rather than hidden by marking the questionnaire
complete.
Day 2 questions are not required for an Incubation application and are not
answered in this snapshot. Operational evidence that already exists may be
added during CNCF review, but it is not needed to make the Day 0 and Day 1
scope complete.
## Day 0 — Planning Phase
### Scope
**How are roadmap scope and mid- to long-term features determined, and how does
that map to contributions and the maintainer ladder?**
Forward-looking goals and target versions are maintained in the public
[`ROADMAP.md`](https://github.com/higress-group/higress/blob/main/ROADMAP.md),
which links the version-controlled website roadmap. Feature scope and roadmap
changes are discussed through public GitHub issues, discussions, and pull
requests. Maintainers use lazy consensus, as documented in
[`GOVERNANCE.md`](https://github.com/higress-group/higress/blob/main/GOVERNANCE.md),
to accept project-direction changes.
Contributors implement accepted work through pull requests, may receive
delegated path-review responsibility as code owners, and may be nominated as
maintainers after sustained contribution under
[`MAINTAINERS.md`](https://github.com/higress-group/higress/blob/main/MAINTAINERS.md).
Objective progression expectations for the intermediate code-owner role remain
a project-maturity gap.
**Who are the target personas?**
- Platform and gateway engineers operating shared application entry points.
- Application teams publishing APIs through Ingress or Gateway API.
- AI platform teams governing access to LLM and MCP services.
- Plugin developers extending request and response processing.
- Security and SRE teams configuring policy, telemetry, and reliability.
**What are the primary, additional, and unsupported use cases?**
The primary use case is routing and governing north-south API traffic. Other
supported use cases include Kubernetes Ingress and Gateway API, service
registry integration, AI/LLM proxying, MCP server exposure, authentication,
rate limiting, observability, and standalone development installations.
Higress is not a transparent east-west service mesh, identity provider, model
provider, general-purpose application runtime, secrets manager, or substitute
for an organization's PKI, security governance, and incident response.
**Which organizations benefit from adoption?**
Organizations operating Kubernetes platforms, microservices, public or
partner APIs, multi-provider AI platforms, or centralized platform-engineering
services can benefit. Public examples are listed in
[`ADOPTERS.md`](https://github.com/higress-group/higress/blob/main/ADOPTERS.md).
**What end-user research has been completed?**
The project records adopter names and use cases in `ADOPTERS.md` and receives
feedback through GitHub and Discord. No independent end-user research report or
published structured interview set is currently available. Adopter interviews
for CNCF Incubation remain to be completed through the official TOC process.
### Usability
**How do target personas interact with the project?**
Cluster administrators install Higress with Helm. Platform teams configure it
with Kubernetes Ingress, Gateway API, Higress and Istio custom resources, the
`hgctl` CLI, or the optional console. Application teams normally submit routes
and policies rather than operate the data plane directly. Plugin developers use
the Wasm SDKs or native filter interfaces.
**What are the UX and UI?**
The primary interface is declarative Kubernetes YAML and Helm values. The
optional Higress Console provides browser-based management of routes, services,
certificates, and plugins. `hgctl` provides command-line operations. Runtime
behavior is observed through logs, metrics, traces, Kubernetes status, and
Envoy administration data where enabled.
**How does Higress integrate with production environments?**
Higress integrates with Kubernetes, Envoy, Istio APIs, Gateway API,
Prometheus-compatible monitoring, OpenTelemetry tracing, OCI registries,
certificate issuers, and service registries such as Nacos, Consul, Eureka, and
ZooKeeper. AI deployments may integrate with identity services, Redis, and
model providers. Except for Kubernetes in the standard Helm deployment, these
integrations are optional and selected by the adopter.
### Design
**What design principles and practices are followed?**
- Declarative, versioned APIs and reconciliation.
- Separation of control plane and Envoy-based data plane.
- xDS dynamic updates without gateway configuration reloads.
- Standard Kubernetes Ingress and Gateway API where possible.
- Independently versioned extensions, with Wasm used as an isolation boundary.
- Compatibility and regression coverage through unit, race, conformance, and
end-to-end tests.
The component architecture and data flow are documented in
[`docs/architecture.md`](https://github.com/higress-group/higress/blob/main/docs/architecture.md).
**What differs between proof-of-concept, development, test, and production?**
The all-in-one image or a minimal single-cluster Helm installation is suitable
for evaluation. CI uses ephemeral kind clusters. Production operators should
pin versions, use multiple gateway replicas, define resource sizing, configure
PodDisruptionBudgets and topology placement, use production PKI, and monitor
traffic and xDS health. The chart defaults to one controller replica and does
not constitute a production high-availability guarantee.
**Which services are required in the cluster?**
The Kubernetes deployment requires the Kubernetes API and DNS. Higress
controller/discovery supplies xDS to the gateway. The console, external service
registries, Redis, certificate issuers, observability backends, OCI registries,
and AI providers are optional.
**How is identity and access management implemented?**
Kubernetes ServiceAccounts and RBAC authorize control-plane access to cluster
resources. TokenReview and SubjectAccessReview APIs are used by inherited Istio
control-plane functions. Gateway-facing identity and policy are configured with
TLS and plugins such as JWT, OIDC, key-auth, HMAC, and basic-auth. Higress does
not operate an identity provider.
**How is sovereignty addressed?**
Higress does not transmit project usage telemetry by default. Operators choose
their image/plugin registries, model providers, observability destinations,
certificate authorities, and data locations. Request data is nevertheless
processed by every configured gateway plugin and upstream provider; operators
are responsible for selecting integrations that meet residency requirements.
**Which compliance requirements are addressed?**
The open-source project does not claim PCI-DSS, SOC 2, ISO 27001, GDPR, or other
regulatory certification. It is Apache-2.0 licensed and CI includes source and
dependency license checks. Deployers must assess their full environment and
configuration.
**What are the high-availability requirements?**
Gateway availability requires multiple replicas or nodes, adequate capacity,
health probes, and failure-domain placement. Controller unavailability stops
new configuration but existing Envoy processes may continue serving their last
accepted configuration. Production control-plane availability requires
multiple appropriately configured replicas and Kubernetes leader-election
behavior. The default single controller is a gap operators must address.
**What are the CPU, network, memory, and storage requirements?**
Current Helm defaults request 500m CPU and 2 GiB memory for the controller and
2 CPU and 2 GiB memory per gateway. Actual requirements depend on route count,
plugin set, configuration churn, request rate, payload sizes, connections, and
telemetry. Operators must size from load tests. Traffic bandwidth and
connection count drive network and socket consumption.
Configuration and certificates are stored as Kubernetes API objects. Gateway
and controller runtime files are ephemeral. The optional console and configured
integrations may introduce separate persistence requirements.
**What is the API design?**
- **Topology and conventions:** Kubernetes Ingress, Gateway API, Istio
networking APIs, and versioned Higress CRDs under `networking.higress.io` and
`extensions.higress.io` are reconciled into xDS resources.
- **Defaults:** Helm defaults create controller/discovery and gateway
deployments, ServiceAccounts, RBAC, Services, and CRDs. Gateway service type
defaults to `LoadBalancer`; automatic HTTPS is enabled in chart values.
- **Additional configuration:** Production use normally requires explicit
resource sizing, exposure, TLS, replicas, credentials, monitoring, and route
policy.
- **New/changed calls:** Enabling registry, certificate, identity, OCI, Redis,
or AI integrations causes calls to the endpoints explicitly configured by
the operator. Enabling Gateway API or inference support causes the controller
to watch and reconcile those API groups.
- **Kubernetes compatibility:** Compatibility is tested with kind and Gateway
API conformance. CRD and Kubernetes-version requirements are carried in Helm
packaging and release dependencies.
- **Versioning and breaking changes:** CRDs have explicit API versions. Stable
breaking changes require migration guidance, deprecation notice, and release
notes. Alpha Gateway API and inference capabilities are explicitly enabled
and do not have the same stability promise as stable APIs.
**What is the release process?**
Higress uses semantic version tags, including release-candidate suffixes when
needed. A `v*.*.*` tag triggers workflows that build controller, pilot and
gateway images, attach generated CRDs, build multi-platform `hgctl` archives,
and generate release notes. Versions are recorded in `VERSION`, Helm chart
metadata, dependency metadata, and `release-notes/`. The normative
[`RELEASE.md`](https://github.com/higress-group/higress/blob/main/RELEASE.md)
defines planning, two-person approval, validation, tagging, publishing,
verification, rollback/correction, security releases, and supported-version
changes.
### Installation
**How is the project installed and initialized?**
```console
helm repo add higress.io https://higress.io/helm-charts
helm repo update
helm install higress -n higress-system higress.io/higress --create-namespace
```
The chart installs CRDs, controller/discovery, gateway, Services,
ServiceAccounts, and RBAC. Operators then apply routes and policies or use the
optional console.
**How does an adopter validate installation?**
Check the Helm release and Ready state of controller and gateway pods, apply a
sample backend and route, then send an HTTP request through the gateway and
inspect status, logs, and metrics. CI performs the same class of installation
in ephemeral clusters and runs Gateway API and Higress conformance traffic.
### Security
**Where is the cloud-native security self-assessment?**
[`docs/cncf/security-self-assessment.md`](./security-self-assessment.md)
**How does Higress address the Cloud Native Security Tenets?**
- **Secure by default:** gateway containers use non-root and restricted
settings on supported Kubernetes platforms, TLS and private Secret delivery
are supported, and optional integrations require explicit configuration.
- **Least privilege:** separate ServiceAccounts and RBAC exist for controller
and gateway. The controller's broad ClusterRole and empty default container
security context are known exceptions requiring improvement.
- **Defense in depth:** Kubernetes RBAC, TLS/SDS, Envoy validation, Wasm
isolation, authentication/policy plugins, and network controls provide
distinct layers.
- **Explicit trust and boundaries:** Kubernetes administrators, xDS,
certificate sources, plugins, registries, identity providers, and upstreams
are separate trust boundaries described in the security self-assessment.
- **Secure lifecycle:** public review, automated tests, license checks, weekly
CodeQL, private advisories, and coordinated disclosure are in place, with
SBOM/signing/dynamic-analysis gaps disclosed.
Operators can loosen security by overriding pod/container security contexts,
using privileged or host networking modes, expanding RBAC, exposing admin
ports, disabling TLS/policy, or installing third-party/native plugins. These
settings should be treated as risk acceptances and tested in the adopter's
threat model.
**What security hygiene is maintained, and which features are high risk if not
maintained?**
The project uses required review, build and unit tests with Go race detection,
Gateway API/Higress conformance, plugin tests, license checks, weekly CodeQL,
versioned dependencies, Private Security Advisories, and coordinated
disclosure. Certificate handling, RBAC reconciliation, xDS generation and
validation, parsers/routing, authentication and authorization plugins, plugin
loading, and release artifacts are treated as security-critical boundaries.
**What privileges are required?**
The gateway reads Kubernetes Secrets for SDS. The controller watches and
reconciles ingress, Gateway API, Higress/Istio, discovery, certificate,
admission, and leader-election resources and performs TokenReview and
SubjectAccessReview calls. Several controller rules are cluster-wide and use
broad resource or verb sets. These are functional requirements of the current
implementation, but the project has not demonstrated that every permission is
minimal.
**How are certificates rotated?**
Gateway certificates are delivered through SDS. Automatic HTTPS can request
and renew ACME certificates, and Kubernetes Secrets can be updated without
gateway configuration reload. Operators remain responsible for issuer trust,
renewal monitoring, emergency rotation, and external certificate systems.
**How is the software supply chain secured?**
Source and dependency licenses are checked; dependencies and submodule commits
are versioned; changes are reviewed and tested; release artifacts are produced
by GitHub Actions from version tags. Some workflow actions are commit-pinned.
Project-generated release SBOMs, universal immutable action pinning, artifact
signatures, and verifiable build provenance are not currently present and are
recorded as gaps in the security self-assessment.
## Day 1 — Installation and Deployment Phase
### Project Installation and Configuration
**What do installation and configuration look like?**
Helm installs the project resources. Values select watched namespaces, replica
counts, resources, autoscaling, Service exposure, security contexts,
observability, Gateway API features, image/plugin registries, and optional
integrations. Operators should pin a chart/application version and keep reviewed
values in version control. Routes, services, credentials, and plugin policy are
then applied as Kubernetes resources or through the console.
### Project Enablement and Rollback
**How is Higress enabled or disabled in a live cluster, and is downtime
required?**
Install the chart and direct selected IngressClasses, GatewayClasses, routes,
DNS, or load-balancer traffic to Higress. Workloads do not require sidecars.
With sufficient replicas, ordinary controller and gateway rolling updates are
designed not to require application downtime. Capacity loss, incompatible
configuration, or a single-replica deployment can cause interruption.
Disable Higress by first moving traffic and route ownership to another entry
point, then uninstalling the Helm release. Existing workload behavior changes
only when traffic or resources select Higress.
**How are enablement and disablement tested?**
Pull-request CI installs Higress into kind and executes conformance and E2E
traffic. There is no dedicated automated migration test that moves live traffic
from another gateway to Higress and back; this is a gap.
**How are created resources cleaned up, including CRDs?**
`helm uninstall` removes chart-owned namespaced and cluster-scoped resources
managed by the release. CRDs and user-authored custom resources must be
inventoried and removed deliberately because automatic deletion could destroy
configuration data.
### Rollout, Upgrade and Rollback Planning
**How is compatibility with Kubernetes and orchestration tools maintained?**
The project updates Kubernetes, Gateway API, Istio, and Envoy dependencies and
uses build, kind, conformance, and E2E workflows on ongoing changes. Supported
versions are communicated through chart/dependency metadata, documentation,
and releases. No fixed public compatibility-review cadence is documented.
**How are rollbacks performed, and how can they fail?**
Operators retain the previous Helm revision, values, configuration, and pinned
images, then use `helm rollback <release> <revision>`. CRD storage/schema
changes, incompatible user configuration, unavailable old images, RBAC changes,
certificate changes, or external dependency changes may prevent a complete
rollback. Control-plane failure may leave existing gateways serving their last
accepted xDS state; data-plane rollout, plugin load failure, or invalid
fail-closed policy can affect live traffic.
**Which metrics should trigger rollback?**
Readiness failures, xDS rejection/NACKs, HTTP 4xx/5xx changes, latency and
timeout regression, gateway/controller restart rate, certificate errors,
plugin load failures, connection failures, and CPU/memory/socket saturation
should be compared with the pre-rollout baseline.
**How are upgrade→downgrade→upgrade paths tested?**
The repository tests clean installation and current-version conformance but
does not contain a dedicated automated upgrade→downgrade→upgrade version
matrix. This is a known Day 1 gap; operators must validate their exact source
and target versions in a non-production cluster.
**How are deprecations and removals communicated?**
They are communicated through GitHub releases, versioned release notes,
documentation, and API/version changes. `RELEASE.md` requires migration and
deprecation guidance in the release pull request and announcement. The project
does not currently define a universal minimum deprecation period for all API
types, which remains a process improvement.
**How are alpha and beta capabilities used during rollout?**
Alpha Gateway API and inference capabilities are selected through explicit
chart values and API versions. Operators should enable them first in a test or
canary environment, monitor the rollback signals above, and avoid relying on
their stability as if they were stable APIs.

View File

@@ -0,0 +1,274 @@
# Higress Governance Review
This is the Higress project's self-assessed Governance Review for its CNCF
Incubation application. It follows the current CNCF TOC
[Governance Review Template](https://github.com/cncf/toc/blob/main/toc_subprojects/project-reviews-subproject/governance-review-template.md).
A CNCF Project Reviews reviewer may amend the assessment and findings during
the public review.
- **Review state:** Project working draft; maintainer approval and CNCF Project
Reviews verification pending
- **Evidence branch:**
[`higress-group/higress@main`](https://github.com/higress-group/higress/tree/main)
- **Template verified:** 2026-07-21 against `cncf/toc` `main`
- **Intended TOC snapshot:**
`projects/higress/governance-review/YYYY-MM-DD.md`
This is the project-maintained working copy. CNCF reviewers may revise the
status and findings before a dated snapshot is archived in `cncf/toc`.
The reviewer should freeze evidence links to the reviewed revision when that
snapshot is archived.
## Summary and Assessment
**Status: Mostly Satisfactory**
Higress has discoverable governance, maintainer, code-owner, contribution,
community, security, and Code of Conduct documents. The project defines
project-wide maintainer responsibility, delegated code ownership, subproject
scope, vendor-neutral decisions, maintainer lifecycle, communication channels,
and security-response roles. The maintainer list includes seven publicly active
people affiliated with four organizations.
The documentation and public evidence satisfy all Governance Review criteria
except one Incubation-required item: Higress does not currently publish an
up-to-date public meeting scheduler or integrate its meetings with the CNCF
calendar. The project should establish that real public meeting infrastructure
before asserting that the Governance Review has no remaining Must-Fix item.
### Executing the Assessment
The self-assessment reviewed the following repository evidence as it exists on
2026-07-21:
- [`GOVERNANCE.md`](https://github.com/higress-group/higress/blob/main/GOVERNANCE.md)
- [`MAINTAINERS.md`](https://github.com/higress-group/higress/blob/main/MAINTAINERS.md)
- [`CODEOWNERS`](https://github.com/higress-group/higress/blob/main/CODEOWNERS)
- [`CONTRIBUTING_EN.md`](https://github.com/higress-group/higress/blob/main/CONTRIBUTING_EN.md)
- [`CODE_OF_CONDUCT.md`](https://github.com/higress-group/higress/blob/main/CODE_OF_CONDUCT.md)
- [`COMMUNITY.md`](https://github.com/higress-group/higress/blob/main/COMMUNITY.md)
- [`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md)
- [`README.md`](https://github.com/higress-group/higress/blob/main/README.md)
- Git history for the governance, maintainer, and ownership files
The review distinguishes documented policy from observed repository evidence.
It does not infer private processes or settings that are not publicly recorded.
### Must-Fix Items
The following issues need to be resolved before the Higress Incubation
application asserts completion of its Governance Review:
1. Publish an up-to-date public meeting scheduler and/or integrate the project
meetings with the CNCF calendar.
### Points of Excellence
- The maintainer roster is public and shows affiliation diversity across
Alibaba Cloud, Trip.com, XinYe Technology, and NVIDIA, with linked public
activity evidence for every listed maintainer.
- Governance distinguishes delegated code ownership from project-wide
maintainer authority and defines role and subproject lifecycle processes.
- The project publishes an authoritative channel inventory and separates
public project decisions from narrowly scoped confidential reporting.
- The named Security Response Team has defined incident roles, independent
review, conflict handling, and escalation.
- Governance explicitly protects vendor-neutral direction through individual
seats, equal voting weight, and conflict disclosure.
### Areas for Improvement
The following are non-blocking at Incubation but would improve long-term
governance maturity:
- Record governance evolution and link decisions to issues or pull requests.
- Publish examples demonstrating the maintainer lifecycle in practice.
- Add objective progression expectations for appointment to the code-owner
role.
- Extend `CODEOWNERS` or equivalent ownership records to each active
subproject and periodically audit coverage.
- Track contributor growth and recruitment trends using the linked CNCF
DevStats metrics, not only point-in-time activity.
---
## Review
This review audits the project's current governance evidence. The project's
Incubation application has not yet been opened; this file is the project's
self-assessment to accompany that application.
### Governance Summary
Higress uses maintainer-led lazy consensus. Significant decisions are recorded
in public issues or pull requests. When consensus cannot be reached,
non-conflicted maintainers may vote publicly and a simple majority of votes
cast decides. Governance defines contributor, code-owner, and maintainer
authority; the maintainer and subproject lifecycles; vendor neutrality; and
function-based teams. Confidential security work is handled by a named
Security Response Team under a public mandate.
### Governance Evolution
**Incubating: Suggested — Partially satisfied.**
`GOVERNANCE.md` and `MAINTAINERS.md` were introduced in April 2026, and
`CODEOWNERS` has changed repeatedly since 2022. This demonstrates change over
time, but the repository does not explain why governance evolved or connect
those changes to project experience and outcomes.
### Discoverability
**Incubating: Suggested — Satisfied.**
The README links the governance, maintainer, contribution, Code of Conduct, and
security documents from its Community section.
### Accuracy and Clarity
**Governance reflects actual activities — Incubating: Suggested — Satisfied.**
Public issue/PR collaboration and lazy consensus are consistent with the
repository workflow. Code-owner authority, security-team operation, and the
absence of a current recurring public meeting are documented without claiming
unobserved processes.
**Vendor-neutral direction — Incubating: Suggested — Satisfied.**
Governance states that roles are held by individuals, prohibits guaranteed
employer seats, vetoes, or preferred decision weight, requires material
conflict disclosure, and evaluates contributions and integrations on community
and technical merit.
### Decisions and Role Assignments
**Leadership, contribution, CNCF, governance, and goal decisions — Incubating:
Suggested — Satisfied.**
Governance applies public lazy consensus and non-conflicted maintainer voting
to leadership, contribution acceptance, goals, requests to CNCF, subproject
scope, and governance changes. The contribution guide and `CODEOWNERS` provide
the day-to-day change-review path.
**Function-based teams — Incubating: Suggested — Satisfied.**
Governance defines the creation and retirement requirements for function-based
teams. `SECURITY.md` names the Security Response Team and defines onboarding,
offboarding, incident assignments, conflicts, independent review, and
escalation.
### Maintainers and Maintainer Lifecycle
**Complete lifecycle — Incubating: Suggested — Satisfied.**
The project documents nomination, public activity review, voluntary departure,
inactivity, removal, emeritus status, and return.
**Lifecycle demonstrated — Incubating: Suggested — Not demonstrated.**
The maintainer file has only one introducing commit in the available history.
No public example of adding, replacing, or moving a maintainer to emeritus is
linked.
**Names, contact, responsibility, affiliation — Incubating: Required —
Satisfied.**
The roster provides names, GitHub contacts, project-wide responsibility
domains, and affiliations for all maintainers.
**Appropriate number of active maintainers — Incubating: Required —
Satisfied.**
Seven maintainers cover the project and its four subprojects. The roster
defines active status and annual public review, and its 2026 review links
public issue and pull-request activity for every listed maintainer during the
preceding 12 months.
**Maintainers from at least two organizations — Incubating: N/A, but met.**
The roster names four affiliations.
### Ownership
**Code and documentation ownership matches governance roles — Incubating:
Required — Satisfied.**
Governance defines code owners as delegated path reviewers, documents their
authority and public appointment/removal process, and makes clear that code
ownership neither grants project-wide governance authority nor limits a
maintainer's project-wide responsibility. This matches `CODEOWNERS`, which
includes maintainers and additional area reviewers.
### Code of Conduct
**Adoption and adherence — Incubating: Required — Satisfied as documented.**
The project adopts the CNCF Code of Conduct in `CODE_OF_CONDUCT.md` and links it
from the README and governance.
**Cross-link from governance — Incubating: Required — Satisfied.**
`GOVERNANCE.md` links both the CNCF Code of Conduct and the project copy.
### Subprojects
**All subprojects listed — Incubating: Required — Satisfied.**
Governance identifies the primary repository and lists `higress-console`,
`higress-standalone`, `plugin-server`, and `wasm-go` as active subprojects. It
also distinguishes upstream forks, integrations, examples, websites, and
other organization repositories from the CNCF project scope.
**Subproject lifecycle — Incubating: Suggested — Satisfied.**
Governance applies the project-wide maintainer roster and public decision
process to subprojects, records each repository's responsibility and status,
and requires leadership, contribution, communication, and lifecycle details
when a subproject is added, removed, transferred, or archived.
### Contributors and Community
**Contributor ladder — Incubating: Suggested — Partially satisfied.**
Governance defines contributor, delegated code-owner, and project-wide
maintainer roles. The contribution and maintainer documents define how
contributors participate and how sustained contributors progress to
maintainership; code-owner assignments are changed publicly through
`CODEOWNERS`. Objective progression expectations for the intermediate
code-owner role can be made more explicit.
**Issue and change submission — Incubating: Required — Satisfied.**
`CONTRIBUTING_EN.md`, issue templates, and the pull-request template document
the process.
**At least one public communication channel — Incubating: Required —
Satisfied.**
GitHub Issues, pull requests, and Discord are publicly documented.
**All public/private channels documented — Incubating: Required — Satisfied.**
`COMMUNITY.md` is the authoritative inventory for project and subproject GitHub
channels, Discord, mailing and localized community channels, documentation,
and narrowly scoped private security and Code of Conduct reporting. It states
that informal or employer-internal conversations are not project decision
channels and requires public decision records.
**Public meeting scheduler/CNCF calendar — Incubating: Required — Not
satisfied.**
No current public meeting scheduler or CNCF calendar integration is linked from
the reviewed repository documents.
**Contribution documentation — Incubating: Required — Satisfied.**
The contribution guide covers issues, pull requests, branches, commits, tests,
style, and AI-assisted contribution requirements.
**Contributor activity and recruitment — Incubating: Required — Satisfied.**
`COMMUNITY.md` links continuously updated GitHub contributor and repository
activity, CNCF DevStats, active-maintainer evidence, `help wanted` and `good
first issue` recruitment queues, the contribution path, and public mentoring
channels.

View File

@@ -0,0 +1,218 @@
# Higress CNCF Incubation Application — Working Draft
This working draft follows the CNCF TOC
[Project Incubation Application v1.6](https://github.com/cncf/toc/blob/main/.github/ISSUE_TEMPLATE/template-incubation-application.md),
verified on 2026-07-21. It is not ready to file while any item labelled
**BLOCKED** below remains unresolved.
## Review Project Moving Level Evaluation
- [ ] I have reviewed the TOC moving-level readiness triage guide, ensured the
criteria are met before opening the issue, and understand that unmet criteria
will result in closure.
This box remains unchecked because the public meeting, access-control evidence,
OpenSSF Passing badge, adopter interviews/verification, and remaining
vendor-neutral resource work are incomplete.
## Project information
- **Project repositories:**
[`higress`](https://github.com/higress-group/higress),
[`higress-console`](https://github.com/higress-group/higress-console),
[`higress-standalone`](https://github.com/higress-group/higress-standalone),
[`plugin-server`](https://github.com/higress-group/plugin-server), and
[`wasm-go`](https://github.com/higress-group/wasm-go)
- **Project site:** <https://higress.ai/en/>
- **Subprojects:** `higress-console`, `higress-standalone`, `plugin-server`,
and `wasm-go`; authoritative scope is in
[`GOVERNANCE.md`](https://github.com/higress-group/higress/blob/main/GOVERNANCE.md)
- **Related projects:** confirm before submission whether any project point of
contact operates a technically related project in another foundation
- **Communication:**
[`COMMUNITY.md`](https://github.com/higress-group/higress/blob/main/COMMUNITY.md)
- **Project points of contact:** Yuanxiao Zhao and Yiquan Dong; **TODO:** confirm
direct contact emails before filing (public community contact:
[higress@googlegroups.com](mailto:higress@googlegroups.com))
## Incubation Criteria Summary
### Application Level Assertion
- [x] Higress is currently Sandbox, accepted on 2026-03-15, and is applying to
Incubation.
- [ ] Higress is applying to join CNCF directly at Incubation level. Not
applicable; remove this option from the filed issue.
### Adoption Assertion
Public production adopters in
[`ADOPTERS.md`](https://github.com/higress-group/higress/blob/main/ADOPTERS.md)
include Ant Digital, Kuaishou, Trip.com, Vipshop, and Labring.
- [ ] **BLOCKED — Adopter interviews:** submit 57 willing adopters through the
official CNCF Adopter Interview Questionnaire before DD assignment. This
requires adopter consent and current contact details; a repository edit
cannot complete it.
## Application Process Principles
### Suggested
- [ ] Engage with the appropriate domain-specific TAG(s) to present the
technical architecture. This is suggested, not a filing prerequisite in
v1.6. A future presentation should be linked here if completed.
### Required
- [x] Complete a General Technical Review. The project self-assessment is in
[`general-technical-review.md`](https://github.com/higress-group/higress/blob/main/docs/cncf/general-technical-review.md);
maintainer approval and CNCF Project Reviews verification are requested.
- [ ] Complete a Governance Review. The self-assessment is in
[`governance-review.md`](https://github.com/higress-group/higress/blob/main/docs/cncf/governance-review.md),
but its public-meeting Must-Fix remains open.
- [ ] **BLOCKED — All project metadata and resources are vendor-neutral.**
Governance and the primary README now document vendor-neutral direction and
no longer promote a single commercial product, but release images remain
hosted only on Alibaba Cloud registry domains, the historical Go module path
contains `github.com/alibaba`, the website roadmap still includes a
single-vendor enterprise mapping, and the security policy requires duplicate
submission to the vendor-operated Alibaba Security Response Center. The
application needs a maintainer/CNCF-reviewed compatibility and infrastructure
plan.
- [x] Review and acknowledgement of Sandbox expectations and maturity-level
requirements. Higress was accepted as a Sandbox project on 2026-03-15 and
this application explicitly acknowledges the current requirements.
- [ ] Due Diligence Review. This is completed through CNCF review, resolution
of concerns, and public comment after a ready application is filed.
- [x] Appropriate installation, end-user, reference, and sample documentation
is linked from the project README and website.
## Governance and Maintainers
### Suggested
- [ ] Complete the Governance Review with the CNCF Project Reviews subproject;
external verification is pending.
- [x] Governance is discoverable and version controlled.
- [x] Governance documents actual public decision, leadership, role, and
security-team processes without claiming a recurring meeting that does not
exist.
- [x] Governance documents vendor-neutral project direction and conflicts.
- [x] Leadership, contribution, CNCF request, governance, goal, and subproject
decisions use a public process.
- [x] Function-based team assignment, onboarding, removal, conflicts, and
retirement are documented.
- [x] The complete maintainer lifecycle is documented.
- [ ] Demonstrate a completed maintainer lifecycle event. Suggested; no public
addition, replacement, or emeritus transition is yet linked.
- [x] Subproject scope, leadership model, contribution, status, and lifecycle
are documented.
### Required
- [x] Current maintainers include names, GitHub contact, responsibility domain,
and affiliation in
[`MAINTAINERS.md`](https://github.com/higress-group/higress/blob/main/MAINTAINERS.md).
- [x] Seven active maintainers are appropriate to the primary repository and
four subprojects; the annual activity review links public evidence for each.
- [x] Code and documentation ownership matches the documented code-owner and
maintainer roles.
- [x] The CNCF Code of Conduct is adopted and linked.
- [x] The Code of Conduct is cross-linked from governance.
- [x] All current subprojects are listed in governance.
## Contributors and Community
### Suggested
- [x] Contributor, code-owner, and maintainer roles form a contributor ladder;
objective code-owner progression criteria remain an improvement item.
### Required
- [x] Issue and change submission are documented.
- [x] Public GitHub and Discord communication channels are documented.
- [x] All official project, subproject, and narrowly scoped non-public channels
are inventoried in `COMMUNITY.md`.
- [ ] **BLOCKED — Public meeting scheduler/CNCF calendar.** Higress does not
currently publish an up-to-date public meeting scheduler or integrate its
meetings with the CNCF calendar. A real schedule or calendar integration must
be established rather than represented by documentation alone.
- [x] Contribution documentation is maintained.
- [x] Contributor activity and recruitment are demonstrated through GitHub,
DevStats, contribution labels, and maintainer activity links.
## Engineering Principles
### Suggested
- [x] The roadmap change process is documented in
[`ROADMAP.md`](https://github.com/higress-group/higress/blob/main/ROADMAP.md).
- [x] Regular release history is public in GitHub Releases and versioned
release notes.
### Required
- [x] Project goals, differentiation, need, and cloud-native use cases are
documented in the README and GTR.
- [x] What Higress does and why it exists are documented in the README and GTR.
- [x] A maintained public roadmap and change process are linked from
`ROADMAP.md`.
- [x] Architecture and software design are documented in
[`docs/architecture.md`](https://github.com/higress-group/higress/blob/main/docs/architecture.md)
and the GTR.
- [x] The release process is documented in
[`RELEASE.md`](https://github.com/higress-group/higress/blob/main/RELEASE.md).
## Security
### Suggested
- [ ] Complete a joint assessment with CNCF TAG Security and Compliance. This
is suggested, not a v1.6 filing prerequisite.
### Required
- [x] Vulnerability reporting is documented in
[`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md).
- [ ] **BLOCKED — Enforced repository access-control evidence.** Public files
do not prove organization 2FA, least-privilege team access, protected default
branch/rulesets, required non-author review, or protected release
environments. An organization owner must verify/configure these settings and
publish reviewable evidence appropriate for CNCF DD.
- [x] Named security-response membership, incident roles, two-person review,
conflicts, report handling, and escalation are documented.
- [x] The Security Self-Assessment is documented in
[`security-self-assessment.md`](https://github.com/higress-group/higress/blob/main/docs/cncf/security-self-assessment.md).
- [ ] **BLOCKED — OpenSSF Best Practices Passing badge.** The public entry is
currently 96%. Warning enforcement/remediation, static-analysis frequency,
confirmed CodeQL alert disposition, and project-level dynamic analysis remain
before the badge can be certified as Passing.
## Ecosystem
### Required
- [x] The public adopter list records organizations, contacts, environment, and
use cases.
- [x] At least three independent adopters report production use; five are
publicly listed.
- [ ] **BLOCKED — TOC adopter verification.** Complete the official interviews
and allow CNCF/TOC reviewers to verify the adopter evidence.
- [x] Integrations and compatibility with Kubernetes, Gateway API, Envoy,
Istio, Prometheus/OpenTelemetry, OCI, service registries, and model providers
are documented in the GTR, architecture, README, and user documentation.
## Current filing decision
**Do not file yet.** The top-level readiness attestation would be false while
the following required items remain open:
1. a real public meeting scheduler and/or CNCF calendar integration;
2. enforced repository access-control evidence;
3. OpenSSF Best Practices Passing, including warning enforcement, analysis
frequency, alert disposition, and dynamic analysis;
4. vendor-neutral resource/website remediation or an accepted migration plan;
5. 57 adopter interview submissions and later TOC verification; and
6. direct project point-of-contact emails confirmed for the issue.

View File

@@ -0,0 +1,282 @@
# Higress Security Self-Assessment
## Metadata
| | |
| --- | --- |
| Assessment stage | Complete working draft, pending maintainer approval; updated 2026-07-21 |
| Software | <https://github.com/higress-group/higress> |
| Review state | Prepared from public project evidence; no independent security review or audit |
| Evidence branch | [`higress-group/higress@main`](https://github.com/higress-group/higress/tree/main) |
| Assessment outline | [CNCF TAG Security Self-Assessment](https://tag-security.cncf.io/community/assessments/guide/self-assessment/) |
| Intended TOC snapshot | `projects/higress/security-assessment/self-assessment.md` |
| Security provider | No. Higress provides security features, but its primary function is API gateway traffic management. |
| Languages | Go, C++, Rust, AssemblyScript, shell, and Helm/YAML |
| SBOM | Not generated for release artifacts. Go module files, Cargo lock data, and container build files provide dependency inputs. |
### Security Links
| Document | Location |
| --- | --- |
| Vulnerability reporting and response | [`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md) |
| Architecture | [`docs/architecture.md`](https://github.com/higress-group/higress/blob/main/docs/architecture.md) |
| Helm defaults | [`helm/core/values.yaml`](https://github.com/higress-group/higress/blob/main/helm/core/values.yaml) |
| OpenSSF Best Practices | <https://www.bestpractices.dev/projects/12667> |
This is the project-maintained working copy. For formal Due Diligence, a vetted
snapshot must be archived at the path above in `cncf/toc`. The reviewer should
freeze evidence links to the reviewed revision when that snapshot is archived.
## Overview
Higress translates declarative ingress, Gateway API, service discovery, and
plugin configuration into xDS consumed by an Envoy-based gateway. It accepts
untrusted downstream traffic, selects upstream services, applies traffic and
security policy, and proxies requests and responses.
### Actors and Actions
- **Cluster administrator:** installs/upgrades Higress, grants RBAC, configures
exposure, certificates, registries, and security contexts.
- **Gateway operator/platform engineer:** creates routes, services, policies,
plugins, credentials, and observability configuration.
- **Application owner:** requests routes/policy and operates upstream services.
- **End user/client:** sends potentially hostile network requests.
- **Plugin author/provider:** supplies code that executes in the gateway's Wasm
sandbox or native filter boundary.
- **External provider/registry:** supplies service discovery data, plugins,
identity metadata, certificates, or AI/model APIs when configured.
- **Project maintainer/release manager:** reviews changes, responds to reports,
and publishes releases.
### Background
Higress combines the Envoy data plane and Istio-derived control plane with
Ingress/Gateway API translation and an extensible plugin ecosystem. The
control plane watches configuration and discovery sources and generates xDS;
the data plane accepts untrusted network traffic and applies that configuration.
### Architecture and Data Flow
The security-relevant flow is:
1. An authenticated Kubernetes user or automation writes Ingress, Gateway API,
Higress/Istio, Secret, and policy resources to the Kubernetes API.
2. Higress controller and discovery components watch authorized resources,
optional service registries, and certificate sources, then translate the
desired state into xDS and SDS configuration.
3. Gateway pilot agents and Envoy processes receive configuration and secret
material. Existing gateways may retain their last accepted configuration if
the control plane becomes unavailable.
4. Untrusted clients connect to the gateway. Envoy and configured filters parse,
authenticate, authorize, transform, and route requests to upstream services
or explicitly configured AI/model providers.
5. The gateway emits access logs, metrics, and traces to destinations selected
by the operator. Depending on configuration, those signals may include
sensitive request metadata.
### Data Assets and Trust Boundaries
| Asset or boundary | Security relevance |
| --- | --- |
| Kubernetes configuration and status | Unauthorized mutation can redirect traffic, weaken policy, load code, or disrupt availability. |
| TLS keys, certificates, tokens, and upstream credentials | Disclosure or replacement can enable impersonation, traffic decryption, or unauthorized upstream access. |
| Request/response bodies, AI prompts, and model outputs | May contain application secrets, personal data, proprietary data, or attacker-controlled content. |
| xDS/SDS control-plane boundary | Integrity and availability determine what code, routes, clusters, policy, and secrets the data plane uses. |
| Gateway process boundary | Native Go/C++ filters execute inside the proxy trust boundary; a defect can affect the full gateway process. |
| Wasm plugin boundary | Wasm provides stronger isolation than a native filter, but plugins can still observe or modify traffic granted to them. |
| External integration boundary | Registries, identity providers, certificate issuers, Redis, observability systems, OCI registries, and model providers have separate operators and trust. |
| Source and release boundary | Compromise of source control, CI identities, dependencies, images, or plugin artifacts can affect every adopter. |
### Goals
- Preserve the integrity and availability of configuration delivery between
control and data planes, reject invalid xDS updates, and make transport trust
assumptions explicit for operators.
- Terminate and originate TLS as configured and distribute private key material
through Kubernetes Secrets and SDS.
- Isolate Wasm plugin execution from the gateway process to the extent provided
by the Envoy/Wasm runtime.
- Enforce configured routing, authentication, authorization, traffic, and data
policies consistently.
- Contact only the external registries, identity systems, observability
backends, AI providers, and upstreams explicitly configured by the operator.
- Provide a private vulnerability reporting and coordinated disclosure path.
### Non-goals
Higress does not secure a compromised Kubernetes control plane, node, cluster
administrator, upstream service, identity provider, model provider, registry,
or native plugin. It does not guarantee that user-authored policy is correct,
provide regulatory certification, or replace network segmentation, secrets
management, PKI governance, application security, or incident response.
## Self-Assessment Use
This document is an internal analysis by the Higress project. It is not an
independent audit, certification, or attestation. It gives adopters and CNCF
reviewers an initial view of security boundaries, practices, and known gaps.
## Security Functions and Features
### Critical
- xDS configuration generation, transport, validation, and last-known-good
behavior between controller/discovery and gateways.
- TLS termination/origination, SDS secret delivery, certificate issuance and
rotation paths, and private-key access.
- Kubernetes RBAC, ServiceAccounts, token reviews, subject-access reviews, and
admission/configuration validation.
- HTTP/TCP parsing, routing, upstream selection, and request/response mutation.
- Plugin loading and Wasm sandbox boundary; native Go/C++ filters share the
gateway process trust boundary.
- Release workflows, container/plugin registries, dependency inputs, and
published artifacts.
### Security Relevant
- Authentication and authorization plugins (JWT, OIDC, key, HMAC, basic auth),
WAF, rate limiting, request blocking, and data masking.
- Pod/container security contexts, host networking, privileged mode, RBAC
toggles, network exposure, and admin/debug endpoints.
- Access, audit-style, metrics, and trace output, which may contain sensitive
request metadata depending on operator configuration.
- External service registries, Redis, certificate issuers, identity providers,
OCI registries, and AI/model providers.
### Threat Model
This project-authored model uses the actors, assets, flows, and boundaries above.
Priority reflects generic impact for a shared production gateway; adopters must
adjust it for their tenancy model and data classification.
| ID | Priority | Threat scenario | Current mitigation and residual risk |
| --- | --- | --- | --- |
| HG-TM-01 | High | A principal with excessive Kubernetes permissions creates or changes routes, policies, Secrets, or plugins to hijack traffic or bypass controls. | Kubernetes authentication, RBAC, API validation, namespace scoping where configured, and review/GitOps practices reduce exposure. Cluster administrators remain trusted, and the controller currently has broad cluster-wide permissions. |
| HG-TM-02 | High | A compromised controller, gateway ServiceAccount, or ClusterRoleBinding exposes TLS keys or upstream credentials. | Secrets are delivered through Kubernetes APIs and SDS rather than embedded in images. Gateway and controller roles are separate, but secret access and several controller rules remain broad and require minimization. |
| HG-TM-03 | High | A malicious or compromised plugin/image executes code, exfiltrates traffic, or alters policy. | Plugin installation is explicit and Wasm supplies a sandbox boundary. Native filters share the gateway process; release/plugin artifacts lack project-wide signature, SBOM, and provenance guarantees. |
| HG-TM-04 | High | Control-plane or xDS/SDS channel compromise injects malicious configuration or secret material, or prevents updates. | Envoy validates delivered resources and can retain last accepted configuration. Operators must protect control-plane identities, endpoints, network paths, and Kubernetes access; transport and deployment assumptions require a dedicated hardening guide. |
| HG-TM-05 | High | Hostile downstream traffic exploits an Envoy/filter parser defect or exhausts connections, memory, CPU, sockets, or upstream capacity. | Envoy validation, resource limits, timeouts, rate-limit/circuit-breaker features, probes, and multiple replicas can limit impact. Safe values are deployment-specific and timely Envoy/Higress patching remains essential. |
| HG-TM-06 | High | A tenant accidentally or deliberately attaches a route or policy to another tenant's gateway and exposes or disrupts traffic. | Kubernetes RBAC, namespace separation, Gateway API attachment controls, and review can restrict authors. Higress does not make a hostile multi-tenant cluster safe when administrators grant overlapping write privileges. |
| HG-TM-07 | High | Requests, AI prompts, credentials, or responses are disclosed to an external registry, plugin, observability backend, identity service, or model provider. | Integrations require operator configuration and can be restricted through credentials and network policy. Operators remain responsible for egress allowlists, provider contracts, log redaction, residency, and data-retention controls. |
| HG-TM-08 | Medium | Logs, metrics, traces, admin/debug endpoints, or configuration dumps expose credentials or sensitive request metadata. | Admin interfaces are not intended as public endpoints and telemetry is configurable. Access control, redaction, retention, and exposure are deployment responsibilities; unsafe logging or endpoint exposure remains possible. |
| HG-TM-09 | High | Certificate expiration, issuer compromise, or unauthorized Secret replacement causes outage or impersonation. | SDS and automatic HTTPS support dynamic certificate updates and renewal. Operators must secure issuers, monitor expiry/renewal, test emergency rotation, and control Secret writers. |
| HG-TM-10 | High | A source-control, dependency, GitHub Actions, registry, or maintainer-account compromise produces malicious release artifacts. | Public review, CI tests, license checks, CodeQL, pinned dependency/submodule inputs, and GitHub release workflows provide layers. Missing universal immutable action pinning, release SBOMs, signatures, provenance, and documented repository access controls leave material residual risk. |
The model should be reviewed after material architecture, privilege, plugin,
release-pipeline, or trust-boundary changes. The project does not yet enforce a
review cadence or require a named security reviewer, which is itself a process
gap.
## Project Compliance
The open-source project does not claim PCI-DSS, SOC 2, ISO 27001, GDPR, or
other regulatory certification. Deployers are responsible for assessing their
configuration and operational environment. Source is Apache-2.0 and pull
requests run license header and dependency-license checks.
## Secure Development Practices
Pull requests are publicly reviewed and run build/unit tests with Go race
detection, Gateway API and Higress conformance tests, plugin tests, and license
checks. CodeQL is scheduled weekly; it is not currently a pull-request gate.
The configured `golangci-lint` execution is commented out because of existing
findings. Release tags trigger image and CLI/CRD artifact builds. Dependency
inputs are versioned, but release artifacts do not currently have a
project-generated SBOM, signature, or SLSA provenance. Not all workflow actions
are pinned to immutable commits. The public repository does not prove a
required reviewer count, signed-commit requirement, organization-wide 2FA, or
branch-protection configuration; those controls require separate repository-
settings evidence.
Ordinary project-team communication uses GitHub issues, pull requests,
discussions, mailing, localized community channels, and Discord. Inbound users
use the same public channels. Every vulnerability report must be submitted to
both GitHub Private Security Advisories and the Alibaba Security Response
Center, as documented in `SECURITY.md`. The Security Response Team correlates
the two private records.
Releases, the project website, and the WeChat Official Account are outbound
channels. [`COMMUNITY.md`](https://github.com/higress-group/higress/blob/main/COMMUNITY.md)
is the authoritative inventory of public, subproject, and narrowly scoped
non-public channels.
Higress operates in the Kubernetes networking and cloud-native gateway
ecosystem. It implements Ingress and Gateway API and builds on Envoy, Istio,
OCI, Prometheus/OpenTelemetry conventions, and optional service registries.
## Security Issue Resolution
[`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md)
prohibits public vulnerability reports and
requires reporters to submit the same substantive report through both GitHub
Private Security Advisories and the Alibaba Security Response Center. The named
Security Response Team is the current maintainer list.
For each case it assigns a triage coordinator, fix lead, independent reviewer
and release lead, and disclosure lead, with at least two unconflicted members.
The policy documents conflicts and escalation. Its targets are acknowledgement
within three business days and triage within 14 days, followed by private fix
development, coordinated disclosure (typically within 90 days), a GitHub
Security Advisory, and a CVE request where appropriate.
An operational security incident in an adopter environment remains the
adopter's responsibility. The project handles defects in project code and
artifacts. For confirmed project vulnerabilities, maintainers triage impact,
develop and release a fix, notify affected users through a GitHub Security
Advisory and release information, and coordinate timing with the reporter.
## Appendix
### Known Gaps
- OpenSSF Passing is not complete. Outstanding areas include compiler warning
enforcement, static-analysis alert remediation, and dynamic analysis.
- There are unresolved critical/high CodeQL alerts requiring maintainer access,
triage, and either fixes or documented upstream/vendor dispositions.
- Release SBOMs, signatures, and verifiable build provenance are absent.
- The controller's ClusterRole is broad and its default container security
context is empty. Gateway non-root defaults depend on Kubernetes/platform
capability; legacy fallback adds `NET_BIND_SERVICE` and allows escalation.
- No dedicated fuzzing/DAST program or automated upgrade/downgrade matrix is
documented.
- This threat model is project-authored, has not been independently validated,
and is not yet backed by a published data-flow diagram with explicit trust
boundaries or an independent security audit.
- Requiring every reporter to duplicate the report in the vendor-operated
Alibaba Security Response Center creates a vendor-neutrality concern that
requires CNCF review or a future neutral replacement plan.
### Known Issues Over Time
Published project advisories are available from the repository's
[Security Advisories](https://github.com/higress-group/higress/security/advisories)
page. This assessment does not claim that the absence of a public advisory for
a period means no vulnerability existed. The project has not published an
aggregate vulnerability history or mean-time-to-remediation report.
### OpenSSF Best Practices
The [Higress OpenSSF entry](https://www.bestpractices.dev/projects/12667) is at
96% of the Passing badge as of this assessment. Seven criteria remain
unanswered or unmet: compiler-warning enforcement, strict-warning enforcement,
warning remediation, static-analysis remediation, static-analysis frequency,
dynamic analysis, and enabling assertions or equivalent dynamic-analysis
checks. Passing requires evidence and implementation for all seven, not merely
updating the questionnaire.
### Example Use Cases
1. A platform team exposes Kubernetes services through Gateway API with TLS,
JWT authentication, rate limiting, and Prometheus metrics.
2. An AI platform routes requests across model providers while applying token
quotas, content policy, and request/response observability.
3. A microservice platform discovers Nacos/Consul services and exposes them
through stable API routes without reloading the data plane.
### Related Projects and Vendors
Envoy supplies the proxy foundation; Istio supplies xDS/control-plane building
blocks; Kubernetes Gateway API and Ingress supply standard configuration APIs.
Kong, Apache APISIX, Envoy Gateway, Traefik, and ingress controllers address
overlapping gateway use cases with different APIs, extension models, and
operational tradeoffs. Commercial products may distribute or manage Higress,
but they are outside this open-source security assessment.

1
go.mod
View File

@@ -128,6 +128,7 @@ require (
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect

4
go.sum
View File

@@ -5345,7 +5345,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.
google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
@@ -5475,8 +5474,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
@@ -5564,7 +5561,6 @@ google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.2.1
appVersion: 2.2.3
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -15,4 +15,4 @@ dependencies:
repository: "file://../redis"
version: 0.0.1
type: application
version: 2.2.1
version: 2.2.3

View File

@@ -20,11 +20,6 @@ template:
{{- end }}
{{- include "gateway.selectorLabels" . | nindent 6 }}
spec:
{{- if .Values.gateway.imagePullPolicy }}
imagePullPolicy: {{ .Values.gateway.imagePullPolicy }}
{{- else if .Values.global.imagePullPolicy }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- end }}
{{- with .Values.gateway.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 6 }}
@@ -45,6 +40,11 @@ template:
containers:
- name: higress-gateway
image: "{{ .Values.gateway.hub | default .Values.global.hub }}/higress/{{ .Values.gateway.image | default "gateway" }}:{{ .Values.gateway.tag | default .Chart.AppVersion }}"
{{- if .Values.gateway.imagePullPolicy }}
imagePullPolicy: {{ .Values.gateway.imagePullPolicy }}
{{- else if .Values.global.imagePullPolicy }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- end }}
args:
- proxy
- router
@@ -211,7 +211,9 @@ template:
{{- $config := $o11y.promtail }}
- name: promtail
image: {{ $config.image.repository | default (printf "%s/higress/promtail" .Values.global.hub) }}:{{ $config.image.tag }}
imagePullPolicy: IfNotPresent
{{- if .Values.global.imagePullPolicy }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- end }}
args:
- -config.file=/etc/promtail/promtail.yaml
env:

View File

@@ -39,6 +39,11 @@ spec:
securityContext:
{{- toYaml .Values.controller.securityContext | nindent 12 }}
image: "{{ .Values.controller.hub | default .Values.global.hub }}/higress/{{ .Values.controller.image | default "higress" }}:{{ .Values.controller.tag | default .Chart.AppVersion }}"
{{- if .Values.controller.imagePullPolicy }}
imagePullPolicy: {{ .Values.controller.imagePullPolicy }}
{{- else if .Values.global.imagePullPolicy }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- end }}
args:
- "serve"
- --gatewaySelectorKey=higress
@@ -49,6 +54,7 @@ spec:
- --enableStatus={{ .Values.global.enableStatus }}
{{- end }}
- --ingressClass={{ .Values.global.ingressClass }}
- --gatewayClass={{ .Values.global.gatewayClass }}
{{- if .Values.global.watchNamespace }}
- --watchNamespace={{ .Values.global.watchNamespace }}
{{- end }}
@@ -77,7 +83,7 @@ spec:
- name: PILOT_ENABLE_GATEWAY_API
value: "{{ .Values.global.enableGatewayAPI }}"
- name: PILOT_ENABLE_ALPHA_GATEWAY_API
value: "{{ .Values.global.enableGatewayAPI }}"
value: "{{ .Values.global.enableAlphaGatewayAPI }}"
{{- if .Values.global.enableInferenceExtension }}
- name: ENABLE_GATEWAY_API_INFERENCE_EXTENSION
value: "true"

View File

@@ -1,8 +1,8 @@
{{- if .Values.global.ingressClass }}
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: {{ .Values.global.ingressClass }}
spec:
controller: higress.io/higress-controller
{{- end }}
{{- if and .Values.global.ingressClass .Values.global.createIngressClass }}
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: {{ .Values.global.ingressClass }}
spec:
controller: higress.io/higress-controller
{{- end }}

View File

@@ -23,7 +23,7 @@ spec:
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: {{ .Values.pluginServer.hub | default .Values.global.hub }}/higress/{{ .Values.pluginServer.image | default "plugin-server" }}:{{ .Values.pluginServer.tag | default "1.0.0" }}
image: {{ .Values.pluginServer.hub | default .Values.global.hub }}/higress/{{ .Values.pluginServer.image | default "plugin-server" }}:{{ .Values.pluginServer.tag | default .Chart.Version }}
{{- if .Values.pluginServer.imagePullPolicy }}
imagePullPolicy: {{ .Values.pluginServer.imagePullPolicy }}
{{- else if .Values.global.imagePullPolicy }}

View File

@@ -25,6 +25,13 @@ global:
# 2. When the ingress class is set empty, the higress controller will watch all ingress
# resources in the k8s cluster.
ingressClass: "higress"
# -- Whether to create the IngressClass resource for global.ingressClass.
# Set this to false when reusing an existing IngressClass, for example during Nginx Ingress migration.
createIngressClass: true
# -- GatewayClassName used by Higress to select Gateway API resources.
# The default value higress uses controllerName higress.io/gateway-controller.
# A custom value, for example higress-internal, uses controllerName higress.io/gateway-controller-higress-internal.
gatewayClass: "higress"
# -- If not empty, Higress Controller will only watch resources in the specified namespace.
# When isolating different business systems using K8s namespace,
# if each namespace requires a standalone gateway instance,
@@ -47,6 +54,8 @@ global:
enableIstioAPI: true
# -- If true, Higress Controller will monitor Gateway API resources as well
enableGatewayAPI: true
# -- If true, Higress Controller will monitor Gateway API resources that have not reached v1 yet
enableAlphaGatewayAPI: false
# -- If true, enable Gateway API Inference Extension support
enableInferenceExtension: false
# -- Used to locate istiod.

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 2.2.1
version: 2.2.3
- name: higress-console
repository: https://higress.io/helm-charts/
version: 2.2.1
digest: sha256:b74e3b6f0b00364a155532fd825398e0ff856f13ec90a256e05bbd9c6bead653
generated: "2026-04-09T17:30:46.726657+08:00"
version: 2.2.3
digest: sha256:b2089e3b943edcf5cfbb52c42fbf00fad58171460a89e5a20c88cd7657f75419
generated: "2026-06-24T19:48:33.129399+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.2.1
appVersion: 2.2.3
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
version: 2.2.1
version: 2.2.3
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 2.2.1
version: 2.2.3
type: application
version: 2.2.1
version: 2.2.3

View File

@@ -161,10 +161,12 @@ The command removes all the Kubernetes components associated with the chart and
| global.caAddress | string | `""` | The customized CA address to retrieve certificates for the pods in the cluster. CSR clients such as the Istio Agent and ingress gateways can use this to specify the CA endpoint. If not set explicitly, default to the Istio discovery address. |
| global.caName | string | `""` | The name of the CA for workload certificates. For example, when caName=GkeWorkloadCertificate, GKE workload certificates will be used as the certificates for workloads. The default value is "" and when caName="", the CA will be configured by other mechanisms (e.g., environmental variable CA_PROVIDER). |
| global.configCluster | bool | `false` | Configure a remote cluster as the config cluster for an external istiod. |
| global.createIngressClass | bool | `true` | Whether to create the IngressClass resource for global.ingressClass. Set this to false when reusing an existing IngressClass, for example during Nginx Ingress migration. |
| global.defaultPodDisruptionBudget | object | `{"enabled":false}` | enable pod disruption budget for the control plane, which is used to ensure Istio control plane components are gradually upgraded or recovered. |
| global.defaultResources | object | `{"requests":{"cpu":"10m"}}` | A minimal set of requested resources to applied to all deployments so that Horizontal Pod Autoscaler will be able to function (if set). Each component can overwrite these default values by adding its own resources block in the relevant section below and setting the desired resources values. |
| global.defaultUpstreamConcurrencyThreshold | int | `10000` | |
| global.disableAlpnH2 | bool | `false` | Whether to disable HTTP/2 in ALPN |
| global.enableAlphaGatewayAPI | bool | `false` | If true, Higress Controller will monitor Gateway API resources that have not reached v1 yet |
| global.enableDeltaXDS | bool | `true` | Whether to enable Istio delta xDS, default is false. |
| global.enableGatewayAPI | bool | `true` | If true, Higress Controller will monitor Gateway API resources as well |
| global.enableH3 | bool | `false` | |
@@ -179,6 +181,7 @@ The command removes all the Kubernetes components associated with the chart and
| global.enableSRDS | bool | `true` | |
| global.enableStatus | bool | `true` | If true, Higress Controller will update the status field of Ingress resources. When migrating from Nginx Ingress, in order to avoid status field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the status field of the corresponding Ingress object. |
| global.externalIstiod | bool | `false` | Configure a remote cluster data plane controlled by an external istiod. When set to true, istiod is not deployed locally and only a subset of the other discovery charts are enabled. |
| global.gatewayClass | string | `"higress"` | GatewayClassName used by Higress to select Gateway API resources. The default value higress uses controllerName higress.io/gateway-controller. A custom value, for example higress-internal, uses controllerName higress.io/gateway-controller-higress-internal. |
| global.hostRDSMergeSubset | bool | `false` | |
| global.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com"` | Default hub (registry) for Higress images. For Higress deployments, images are pulled from: {hub}/higress/{image} For built-in plugins, images are pulled from: {hub}/{pluginNamespace}/{plugin-name} Change this to use a mirror registry closer to your deployment region for faster image pulls. |
| global.imagePullPolicy | string | `""` | Specify image pull policy if default behavior isn't desired. Default behavior: latest images will be Always else IfNotPresent. |

View File

@@ -145,7 +145,9 @@ helm delete higress -n higress-system
| gateway.service.ports[1].protocol | string | `"TCP"` | 协议 |
| gateway.service.ports[1].targetPort | int | `443` | 靶向端口 |
| gateway.service.type | string | `"LoadBalancer"` | 服务类型 |
| global.createIngressClass | bool | `true` | 是否为 global.ingressClass 创建 IngressClass 资源。复用已有 IngressClass 时可设置为 false例如从 Nginx Ingress 迁移场景。 |
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
| global.enableAlphaGatewayAPI | bool | `false` | 是否监听 alpha 阶段的 Gateway API 资源 |
| global.enableInferenceExtension | bool | `false` | 是否启用 Gateway API Inference Extension 支持 |
| ... | ... | ... | ... |

View File

@@ -110,6 +110,7 @@ type ServerArgs struct {
// 2. When the ingress class is set empty, the higress controller will watch all ingress
// resources in the k8s cluster.
IngressClass string
GatewayClass string
EnableStatus bool
WatchNamespace string
GrpcKeepAliveOptions *keepalive.Options
@@ -222,6 +223,7 @@ func (s *Server) initConfigController() error {
Enable: true,
ClusterId: s.RegistryOptions.KubeOptions.ClusterID,
IngressClass: s.IngressClass,
GatewayClass: s.GatewayClass,
WatchNamespace: s.WatchNamespace,
EnableStatus: s.EnableStatus,
SystemNamespace: higressconfig.PodNamespace,

View File

@@ -106,6 +106,7 @@ func getServerCommand() *cobra.Command {
serveCmd.PersistentFlags().StringVar(&serverArgs.GatewaySelectorValue, "gatewaySelectorValue", "higress-system-higress-gateway", "gateway resource selector label value")
serveCmd.PersistentFlags().BoolVar(&serverArgs.EnableStatus, "enableStatus", true, "enable the ingress status syncer which use to update the ip in ingress's status")
serveCmd.PersistentFlags().StringVar(&serverArgs.IngressClass, "ingressClass", innerconstants.DefaultIngressClass, "if not empty, only watch the ingresses have the specified class, otherwise watch all ingresses")
serveCmd.PersistentFlags().StringVar(&serverArgs.GatewayClass, "gatewayClass", innerconstants.DefaultGatewayClass, "if not empty, only process Gateway API resources that belong to the specified GatewayClass")
serveCmd.PersistentFlags().StringVar(&serverArgs.WatchNamespace, "watchNamespace", "", "if not empty, only wath the ingresses in the specified namespace, otherwise watch in all namespacees")
serveCmd.PersistentFlags().BoolVar(&serverArgs.Debug, "debug", serverArgs.Debug, "if true, enables more debug http api")
serveCmd.PersistentFlags().StringVar(&serverArgs.HttpAddress, "httpAddress", serverArgs.HttpAddress, "the http address")

View File

@@ -46,6 +46,8 @@ import (
"istio.io/istio/pkg/log"
"istio.io/istio/pkg/util/sets"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
@@ -438,6 +440,7 @@ func (m *IngressConfig) convertGateways(configs []common.WrapperConfig) []config
if err != nil {
IngressLog.Errorf("Get higress https configmap err %v", err)
}
m.preparePassthroughTLSHostOwners(&convertOptions, configs)
for idx := range configs {
cfg := configs[idx]
clusterId := common.GetClusterId(cfg.Config.Annotations)
@@ -504,6 +507,8 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
}
}
m.preparePassthroughTLSHostOwners(&convertOptions, configs)
// convert http route
for idx := range configs {
cfg := configs[idx]
@@ -570,13 +575,8 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
m.ingressRouteCache = convertOptions.IngressRouteCache.Extract()
m.mutex.Unlock()
// Convert http route to virtual service
out := make([]config.Config, 0, len(convertOptions.HTTPRoutes))
for host, routes := range convertOptions.HTTPRoutes {
if len(routes) == 0 {
continue
}
out := make([]config.Config, 0, len(convertOptions.VirtualServices))
for host, wrapperVS := range convertOptions.VirtualServices {
cleanHost := common.CleanHost(host)
// namespace/name, name format: (istio cluster id)-host
gateways := []string{
@@ -585,13 +585,10 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost),
}
wrapperVS, exist := convertOptions.VirtualServices[host]
if !exist {
IngressLog.Warnf("virtual service for host %s does not exist.", host)
}
vs := wrapperVS.VirtualService
vs.Gateways = gateways
routes := convertOptions.HTTPRoutes[host]
// Sort, exact -> prefix -> regex
common.SortHTTPRoutes(routes)
@@ -599,14 +596,18 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
vs.Http = append(vs.Http, route.HTTPRoute)
}
firstRoute := routes[0]
if len(vs.Http) == 0 && len(vs.Tls) == 0 {
continue
}
vsName, clusterId := virtualServiceNameAndClusterID(cleanHost, wrapperVS, routes)
out = append(out, config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.VirtualService,
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, firstRoute.WrapperConfig.Config.Namespace, firstRoute.WrapperConfig.Config.Name, cleanHost),
Name: vsName,
Namespace: m.namespace,
Annotations: map[string]string{
common.ClusterIdAnnotation: firstRoute.ClusterId.String(),
common.ClusterIdAnnotation: clusterId.String(),
},
},
Spec: vs,
@@ -625,6 +626,129 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
return out
}
func virtualServiceNameAndClusterID(cleanHost string, wrapperVS *common.WrapperVirtualService, routes []*common.WrapperHTTPRoute) (string, cluster.ID) {
if len(routes) > 0 {
firstRoute := routes[0]
return common.CreateConvertedName(constants.IstioIngressGatewayName, firstRoute.WrapperConfig.Config.Namespace, firstRoute.WrapperConfig.Config.Name, cleanHost), firstRoute.ClusterId
}
cfg := wrapperVS.WrapperConfig.Config
return common.CreateConvertedName(constants.IstioIngressGatewayName, cfg.Namespace, cfg.Name, cleanHost), common.GetClusterId(cfg.Annotations)
}
func (m *IngressConfig) preparePassthroughTLSHostOwners(convertOptions *common.ConvertOptions, configs []common.WrapperConfig) {
if convertOptions.PassthroughTLSHostOwners == nil {
convertOptions.PassthroughTLSHostOwners = map[string]*config.Config{}
}
// ingress-nginx enables SSL passthrough at host level when any ingress for the host has the
// annotation, then uses the first root path as the passthrough backend.
passthroughHosts := map[string]struct{}{}
firstRootPathHostOwners := map[string]*config.Config{}
for idx := range configs {
cfg := configs[idx]
if cfg.AnnotationsConfig.IsCanary() {
continue
}
if cfg.AnnotationsConfig.IsSSLPassthrough() {
for _, host := range ingressRuleHosts(cfg.Config.Spec) {
passthroughHosts[host] = struct{}{}
}
}
for _, host := range ingressRootPathHosts(cfg.Config.Spec) {
if _, exist := firstRootPathHostOwners[host]; exist {
continue
}
firstRootPathHostOwners[host] = cfg.Config
}
}
for host := range passthroughHosts {
if owner := firstRootPathHostOwners[host]; owner != nil {
convertOptions.PassthroughTLSHostOwners[host] = owner
}
}
}
func ingressRuleHosts(spec config.Spec) []string {
switch ingressSpec := spec.(type) {
case networkingv1.IngressSpec:
return ingressV1RuleHosts(ingressSpec.Rules)
case networkingv1beta1.IngressSpec:
return ingressV1Beta1RuleHosts(ingressSpec.Rules)
default:
return nil
}
}
func ingressRootPathHosts(spec config.Spec) []string {
switch ingressSpec := spec.(type) {
case networkingv1.IngressSpec:
return ingressV1RootPathHosts(ingressSpec.Rules)
case networkingv1beta1.IngressSpec:
return ingressV1Beta1RootPathHosts(ingressSpec.Rules)
default:
return nil
}
}
func ingressV1RuleHosts(rules []networkingv1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
out = append(out, rule.Host)
}
return out
}
func ingressV1Beta1RuleHosts(rules []networkingv1beta1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
out = append(out, rule.Host)
}
return out
}
func ingressV1RootPathHosts(rules []networkingv1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
if rule.HTTP == nil || !hasV1RootHTTPIngressPath(rule.HTTP.Paths) {
continue
}
out = append(out, rule.Host)
}
return out
}
func ingressV1Beta1RootPathHosts(rules []networkingv1beta1.IngressRule) []string {
out := make([]string, 0, len(rules))
for _, rule := range rules {
if rule.HTTP == nil || !hasV1Beta1RootHTTPIngressPath(rule.HTTP.Paths) {
continue
}
out = append(out, rule.Host)
}
return out
}
func hasV1RootHTTPIngressPath(paths []networkingv1.HTTPIngressPath) bool {
for _, path := range paths {
if path.Path == "" || path.Path == "/" {
return true
}
}
return false
}
func hasV1Beta1RootHTTPIngressPath(paths []networkingv1beta1.HTTPIngressPath) bool {
for _, path := range paths {
if path.Path == "" || path.Path == "/" {
return true
}
}
return false
}
func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions) {
var envoyFilters []config.Config
mappings := map[string]*common.Rule{}

View File

@@ -23,6 +23,7 @@ import (
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/constants"
"istio.io/istio/pkg/config/schema/gvk"
"istio.io/istio/pkg/config/xds"
ingress "k8s.io/api/networking/v1"
@@ -109,6 +110,405 @@ func TestNormalizeWeightedCluster(t *testing.T) {
}
}
func TestVirtualServiceNameAndClusterID(t *testing.T) {
cleanHost := common.CleanHost("example.com")
wrapperVS := &common.WrapperVirtualService{
WrapperConfig: &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "tls-ns",
Name: "tls-ingress",
Annotations: map[string]string{
common.ClusterIdAnnotation: "tls-cluster",
},
},
},
},
}
routes := []*common.WrapperHTTPRoute{
{
WrapperConfig: &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "http-ns",
Name: "http-ingress",
},
},
},
ClusterId: "http-cluster",
},
}
name, clusterID := virtualServiceNameAndClusterID(cleanHost, wrapperVS, routes)
if name != common.CreateConvertedName(constants.IstioIngressGatewayName, "http-ns", "http-ingress", cleanHost) {
t.Fatalf("http-backed virtual service name mismatch: %s", name)
}
if clusterID != "http-cluster" {
t.Fatalf("http-backed cluster id mismatch: %s", clusterID)
}
name, clusterID = virtualServiceNameAndClusterID(cleanHost, wrapperVS, nil)
if name != common.CreateConvertedName(constants.IstioIngressGatewayName, "tls-ns", "tls-ingress", cleanHost) {
t.Fatalf("tls-only virtual service name mismatch: %s", name)
}
if clusterID != "tls-cluster" {
t.Fatalf("tls-only cluster id mismatch: %s", clusterID)
}
}
func TestPreparePassthroughTLSHostOwnersRequiresPassthroughHost(t *testing.T) {
m := &IngressConfig{}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "plain-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "plain-root-duplicate",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
}
options := &common.ConvertOptions{}
m.preparePassthroughTLSHostOwners(options, configs)
if len(options.PassthroughTLSHostOwners) != 0 {
t.Fatalf("unexpected ssl passthrough owners: %+v", options.PassthroughTLSHostOwners)
}
}
func TestPreparePassthroughTLSHostOwnersUsesFirstRootPathOwner(t *testing.T) {
m := &IngressConfig{}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "plain-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "passthrough-non-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/api"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
},
}
options := &common.ConvertOptions{}
m.preparePassthroughTLSHostOwners(options, configs)
if !common.IsPassthroughTLSHostOwner(options, configs[0].Config, "example.com") {
t.Fatal("first root ingress was not recorded as passthrough owner")
}
if !common.HasPassthroughTLSHostOwner(options, configs[0].Config) {
t.Fatal("first root ingress was not found as passthrough owner")
}
}
func TestPreparePassthroughTLSHostOwnersIgnoresHTTPOnlyIngressForHTTPSFallback(t *testing.T) {
m := &IngressConfig{}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "http-only",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/api"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-ingress",
},
Spec: ingress.IngressSpec{
TLS: []ingress.IngressTLS{
{
Hosts: []string{"example.com"},
SecretName: "example-com",
},
},
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/app"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
}
options := &common.ConvertOptions{}
m.preparePassthroughTLSHostOwners(options, configs)
if len(options.PassthroughTLSHostOwners) != 0 {
t.Fatalf("unexpected ssl passthrough owners: %+v", options.PassthroughTLSHostOwners)
}
}
func TestConvertGatewaysHonorsFirstRootPathSSLPassthroughOwner(t *testing.T) {
fake := kube.NewFakeClient()
options := common.Options{
Enable: true,
ClusterId: "ingress-v1",
RawClusterId: "ingress-v1__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
ingressController := controllerv1.NewController(fake, fake, options, nil)
m := NewIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
"ingress-v1": ingressController,
}
configs := []common.WrapperConfig{
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-non-root",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
},
},
Spec: ingress.IngressSpec{
TLS: []ingress.IngressTLS{
{
Hosts: []string{"example.com"},
SecretName: "example-com",
},
},
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/api"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
},
{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "passthrough-root",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
},
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{Path: "/"},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
},
}
result := m.convertGateways(configs)
if len(result) != 1 {
t.Fatalf("gateway count mismatch, want 1, got %d", len(result))
}
gateway := result[0].Spec.(*networking.Gateway)
if len(gateway.Servers) != 2 {
t.Fatalf("server count mismatch, want 2, got %d", len(gateway.Servers))
}
tlsServer := gateway.Servers[1]
if tlsServer.Port.Protocol != "TLS" {
t.Fatalf("tls server protocol mismatch, want TLS, got %s", tlsServer.Port.Protocol)
}
if tlsServer.Tls.GetMode() != networking.ServerTLSSettings_PASSTHROUGH {
t.Fatalf("tls mode mismatch, want PASSTHROUGH, got %s", tlsServer.Tls.GetMode())
}
}
func TestConvertGatewaysUsesFirstRootOwnerWhenLaterIngressEnablesSSLPassthrough(t *testing.T) {
fake := kube.NewFakeClient()
options := common.Options{
Enable: true,
ClusterId: "ingress-v1",
RawClusterId: "ingress-v1__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
ingressController := controllerv1.NewController(fake, fake, options, nil)
m := NewIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
"ingress-v1": ingressController,
}
configs := []common.WrapperConfig{
ingressV1Wrapper("root", "example.com", "/", false),
ingressV1Wrapper("passthrough", "example.com", "/passthrough", true),
}
result := m.convertGateways(configs)
if len(result) != 1 {
t.Fatalf("gateway count mismatch, want 1, got %d", len(result))
}
gateway := result[0].Spec.(*networking.Gateway)
if len(gateway.Servers) != 2 {
t.Fatalf("server count mismatch, want 2, got %d", len(gateway.Servers))
}
tlsServer := gateway.Servers[1]
if tlsServer.Port.Protocol != "TLS" {
t.Fatalf("tls server protocol mismatch, want TLS, got %s", tlsServer.Port.Protocol)
}
if tlsServer.Tls.GetMode() != networking.ServerTLSSettings_PASSTHROUGH {
t.Fatalf("tls mode mismatch, want PASSTHROUGH, got %s", tlsServer.Tls.GetMode())
}
}
func TestConvertVirtualServiceUsesFirstRootOwnerWhenLaterIngressEnablesSSLPassthrough(t *testing.T) {
fake := kube.NewFakeClient()
options := common.Options{
Enable: true,
ClusterId: "ingress-v1",
RawClusterId: "ingress-v1__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
ingressController := controllerv1.NewController(fake, fake, options, nil)
m := NewIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
"ingress-v1": ingressController,
}
configs := []common.WrapperConfig{
ingressV1Wrapper("root", "example.com", "/", false),
ingressV1Wrapper("passthrough", "example.com", "/passthrough", true),
}
result := m.convertVirtualService(configs)
if len(result) != 1 {
t.Fatalf("virtual service count mismatch, want 1, got %d", len(result))
}
vs := result[0].Spec.(*networking.VirtualService)
if len(vs.Tls) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(vs.Tls))
}
if got := vs.Tls[0].Route[0].Destination.Host; got != "root.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, want root.default.svc.cluster.local, got %s", got)
}
}
func TestConvertGatewaysForIngress(t *testing.T) {
fake := kube.NewFakeClient()
v1Beta1Options := common.Options{
@@ -616,3 +1016,46 @@ func TestConstructBasicAuthEnvoyFilter(t *testing.T) {
target := proto.Clone(pb).(*httppb.HttpFilter)
t.Log(target)
}
func ingressV1Wrapper(name, host, path string, sslPassthrough bool) common.WrapperConfig {
wrapper := common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: name,
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
},
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: host,
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: path,
Backend: ingress.IngressBackend{
Service: &ingress.IngressServiceBackend{
Name: name,
Port: ingress.ServiceBackendPort{Number: 443},
},
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
Match: &annotations.MatchConfig{},
},
}
if sslPassthrough {
wrapper.AnnotationsConfig.SSLPassthrough = &annotations.SSLPassthroughConfig{Enabled: true}
}
return wrapper
}

View File

@@ -57,6 +57,8 @@ type Ingress struct {
DownstreamTLS *DownstreamTLSConfig
SSLPassthrough *SSLPassthroughConfig
Canary *CanaryConfig
IPAccessControl *IPAccessControlConfig
@@ -115,6 +117,10 @@ func (i *Ingress) IsCanary() bool {
return i.Canary.Enabled
}
func (i *Ingress) IsSSLPassthrough() bool {
return i.SSLPassthrough != nil && i.SSLPassthrough.Enabled
}
// CanaryKind return byHeader, byWeight
func (i *Ingress) CanaryKind() (bool, bool) {
if !i.IsCanary() {
@@ -157,6 +163,7 @@ func NewAnnotationHandlerManager() AnnotationHandler {
canary{},
cors{},
downstreamTLS{},
sslPassthrough{},
redirect{},
rewrite{},
upstreamTLS{},

View File

@@ -106,6 +106,9 @@ func (d downstreamTLS) ApplyGateway(gateway *networking.Gateway, config *Ingress
downstreamTLSConfig := config.DownstreamTLS
for _, server := range gateway.Servers {
if gatewaytool.IsTLSServer(server) {
if server.Tls != nil && server.Tls.Mode == networking.ServerTLSSettings_PASSTHROUGH {
continue
}
if downstreamTLSConfig.CASecretName.Name != "" {
serverCert := extraSecret(server.Tls.CredentialName)
if downstreamTLSConfig.CASecretName.Namespace != serverCert.Namespace ||

View File

@@ -269,6 +269,40 @@ func TestApplyGateway(t *testing.T) {
},
},
},
{
name: "skip passthrough server",
input: &networking.Gateway{
Servers: []*networking.Server{
{
Port: &networking.Port{
Protocol: "TLS",
},
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_PASSTHROUGH,
},
},
},
},
config: &Ingress{
DownstreamTLS: &DownstreamTLSConfig{
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
MinVersion: "TLSv1.2",
MaxVersion: "TLSv1.3",
},
},
expect: &networking.Gateway{
Servers: []*networking.Server{
{
Port: &networking.Port{
Protocol: "TLS",
},
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_PASSTHROUGH,
},
},
},
},
},
}
for _, tc := range testCases {

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package annotations
const sslPassthroughAnnotation = "ssl-passthrough"
var _ Parser = &sslPassthrough{}
type SSLPassthroughConfig struct {
Enabled bool
}
type sslPassthrough struct{}
func (s sslPassthrough) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
enabled, err := annotations.ParseBoolASAP(sslPassthroughAnnotation)
if err != nil {
return nil
}
config.SSLPassthrough = &SSLPassthroughConfig{Enabled: enabled}
return nil
}

View File

@@ -0,0 +1,112 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package annotations
import "testing"
func TestSSLPassthroughParse(t *testing.T) {
testCases := []struct {
name string
input Annotations
enabled bool
exists bool
}{
{
name: "missing",
input: Annotations{},
},
{
name: "enabled by nginx annotation",
input: Annotations{
buildNginxAnnotationKey(sslPassthroughAnnotation): "true",
},
enabled: true,
exists: true,
},
{
name: "enabled by higress annotation",
input: Annotations{
buildHigressAnnotationKey(sslPassthroughAnnotation): "true",
},
enabled: true,
exists: true,
},
{
name: "disabled by nginx annotation",
input: Annotations{
buildNginxAnnotationKey(sslPassthroughAnnotation): "false",
},
exists: true,
},
{
name: "disabled by higress annotation",
input: Annotations{
buildHigressAnnotationKey(sslPassthroughAnnotation): "false",
},
exists: true,
},
}
parser := sslPassthrough{}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &Ingress{}
if err := parser.Parse(tc.input, config, nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if tc.exists && config.SSLPassthrough == nil {
t.Fatal("expected ssl passthrough config")
}
if !tc.exists && config.SSLPassthrough != nil {
t.Fatal("unexpected ssl passthrough config")
}
if tc.exists && config.SSLPassthrough.Enabled != tc.enabled {
t.Fatalf("enabled mismatch, want %v, got %v", tc.enabled, config.SSLPassthrough.Enabled)
}
})
}
}
func TestSSLPassthroughDoesNotSetUpstreamTLS(t *testing.T) {
parser := sslPassthrough{}
config := &Ingress{}
err := parser.Parse(Annotations{
buildNginxAnnotationKey(sslPassthroughAnnotation): "true",
}, config, nil)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if config.UpstreamTLS != nil {
t.Fatal("unexpected upstream tls config")
}
}
func TestSSLPassthroughKeepsExplicitBackendProtocol(t *testing.T) {
manager := NewAnnotationHandlerManager()
config := &Ingress{}
err := manager.Parse(Annotations{
buildNginxAnnotationKey(sslPassthroughAnnotation): "true",
buildNginxAnnotationKey(backendProtocol): "HTTPS",
}, config, nil)
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
if config.UpstreamTLS == nil {
t.Fatal("expected upstream tls config")
}
if config.UpstreamTLS.BackendProtocol != "HTTPS" {
t.Fatalf("backend protocol mismatch, want HTTPS, got %s", config.UpstreamTLS.BackendProtocol)
}
}

View File

@@ -170,9 +170,6 @@ func processMTLS(config *Ingress) *networking.ClientTLSSettings {
func processSimple(config *Ingress) *networking.ClientTLSSettings {
tls := &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
InsecureSkipVerify: &wrappers.BoolValue{
Value: true,
},
}
if config.UpstreamTLS.EnableSNI && config.UpstreamTLS.SNI != "" {

View File

@@ -17,10 +17,8 @@ package annotations
import (
"testing"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/testing/protocmp"
networking "istio.io/api/networking/v1alpha3"
)
@@ -131,9 +129,6 @@ func TestApplyTrafficPolicy(t *testing.T) {
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
Sni: "SNI",
InsecureSkipVerify: &wrappers.BoolValue{
Value: true,
},
},
},
},
@@ -163,9 +158,7 @@ func TestApplyTrafficPolicy(t *testing.T) {
for _, testCase := range testCases {
t.Run("", func(t *testing.T) {
parser.ApplyTrafficPolicy(nil, testCase.input, testCase.config)
if diff := cmp.Diff(testCase.expect, testCase.input, protocmp.Transform(),
cmpopts.IgnoreUnexported(unexportedIgnoredTypes...),
); diff != "" {
if diff := cmp.Diff(testCase.expect, testCase.input, cmpopts.IgnoreUnexported(unexportedIgnoredTypes...)); diff != "" {
t.Fatalf("TestApplyTrafficPolicy() mismatch (-want +got): \n%s", diff)
}
})

View File

@@ -15,6 +15,7 @@
package common
import (
"strconv"
"strings"
"time"
@@ -23,6 +24,7 @@ import (
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
gatewaytool "istio.io/istio/pkg/config/gateway"
"istio.io/istio/pkg/config/protocol"
listerv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
@@ -78,6 +80,20 @@ func (w *WrapperGateway) IsHTTPS() bool {
return false
}
func CreateSSLPassthroughServer(host string, port uint32, clusterId cluster.ID) *networking.Server {
return &networking.Server{
Port: &networking.Port{
Number: port,
Protocol: string(protocol.TLS),
Name: CreateConvertedName("tls-"+strconv.FormatUint(uint64(port), 10)+"-ingress", clusterId.String()),
},
Hosts: []string{WildcardHost(host)},
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_PASSTHROUGH,
},
}
}
type WrapperHTTPRoute struct {
HTTPRoute *networking.HTTPRoute
WrapperConfig *WrapperConfig
@@ -111,6 +127,50 @@ type WrapperVirtualService struct {
AppRoot string
}
func (w *WrapperVirtualService) HasTLSRouteForHost(host string) bool {
if w == nil || w.VirtualService == nil {
return false
}
host = WildcardHost(host)
for _, route := range w.VirtualService.Tls {
for _, match := range route.Match {
for _, sniHost := range match.SniHosts {
if WildcardHost(sniHost) == host {
return true
}
}
}
}
return false
}
func NewWrapperVirtualService(host string, wrapper *WrapperConfig) *WrapperVirtualService {
return &WrapperVirtualService{
VirtualService: &networking.VirtualService{
Hosts: []string{WildcardHost(host)},
},
WrapperConfig: wrapper,
}
}
func CreateTLSRoute(host string, routeDestination []*networking.RouteDestination) *networking.TLSRoute {
return &networking.TLSRoute{
Match: []*networking.TLSMatchAttributes{
{
SniHosts: []string{WildcardHost(host)},
},
},
Route: routeDestination,
}
}
func WildcardHost(host string) string {
if host == "" {
return "*"
}
return host
}
type WrapperTrafficPolicy struct {
TrafficPolicy *networking.TrafficPolicy
PortTrafficPolicy *networking.TrafficPolicy_PortTrafficPolicy

View File

@@ -145,6 +145,41 @@ func (i *IngressDomainCache) Extract() model.IngressDomainCollection {
}
}
func SameConfig(left *config.Config, right *config.Config) bool {
if left == nil || right == nil {
return left == right
}
return GetClusterId(left.Annotations) == GetClusterId(right.Annotations) &&
left.Namespace == right.Namespace &&
left.Name == right.Name
}
func IsPassthroughTLSHostOwner(convertOptions *ConvertOptions, cfg *config.Config, host string) bool {
if convertOptions == nil || convertOptions.PassthroughTLSHostOwners == nil {
return true
}
return SameConfig(convertOptions.PassthroughTLSHostOwners[host], cfg)
}
func PassthroughTLSHostOwner(convertOptions *ConvertOptions, host string) *config.Config {
if convertOptions == nil || len(convertOptions.PassthroughTLSHostOwners) == 0 {
return nil
}
return convertOptions.PassthroughTLSHostOwners[host]
}
func HasPassthroughTLSHostOwner(convertOptions *ConvertOptions, cfg *config.Config) bool {
if convertOptions == nil || len(convertOptions.PassthroughTLSHostOwners) == 0 {
return false
}
for _, owner := range convertOptions.PassthroughTLSHostOwners {
if SameConfig(owner, cfg) {
return true
}
}
return false
}
type ConvertOptions struct {
HostWithRule2Ingress map[string]*config.Config
@@ -167,6 +202,9 @@ type ConvertOptions struct {
CanaryIngresses []*WrapperConfig
// Host to the first root-path ingress owner for hosts that have TLS passthrough enabled.
PassthroughTLSHostOwners map[string]*config.Config
Service2TrafficPolicy map[ServiceKey]*WrapperTrafficPolicy
ServiceWrappers map[string]*ServiceWrapper

View File

@@ -18,10 +18,48 @@ import (
"testing"
"github.com/stretchr/testify/assert"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
)
func TestWildcardHostForSSLPassthrough(t *testing.T) {
server := CreateSSLPassthroughServer("", 443, "")
assert.Equal(t, []string{"*"}, server.Hosts)
vs := NewWrapperVirtualService("", &WrapperConfig{})
assert.Equal(t, []string{"*"}, vs.VirtualService.Hosts)
route := CreateTLSRoute("", []*networking.RouteDestination{{Weight: 100}})
assert.Equal(t, []string{"*"}, route.Match[0].SniHosts)
vs.VirtualService.Tls = append(vs.VirtualService.Tls, route)
assert.True(t, vs.HasTLSRouteForHost(""))
}
func TestPassthroughTLSHostOwnerNilMapAllowsStandaloneConversion(t *testing.T) {
cfg := &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough",
},
}
// A nil owner map means the caller did not prepare ownership from the full ingress snapshot.
assert.True(t, IsPassthroughTLSHostOwner(&ConvertOptions{}, cfg, "example.com"))
assert.Nil(t, PassthroughTLSHostOwner(&ConvertOptions{}, "example.com"))
// A non-nil owner map means ownership has been prepared and missing hosts have no owner.
options := &ConvertOptions{
PassthroughTLSHostOwners: map[string]*config.Config{},
}
assert.False(t, IsPassthroughTLSHostOwner(options, cfg, "example.com"))
assert.Nil(t, PassthroughTLSHostOwner(options, "example.com"))
options.PassthroughTLSHostOwners["example.com"] = cfg
assert.True(t, IsPassthroughTLSHostOwner(options, cfg, "example.com"))
assert.Equal(t, cfg, PassthroughTLSHostOwner(options, "example.com"))
}
func TestIngressDomainCache(t *testing.T) {
cache := NewIngressDomainCache()
assert.NotNil(t, cache)

View File

@@ -364,6 +364,8 @@ func getLoadBalancerIp(svc *v1.Service) []string {
hostName := strings.TrimSuffix(ingress.Hostname, SvcHostNameSuffix)
if net.ParseIP(hostName) != nil {
out = append(out, hostName)
} else {
out = append(out, ingress.Hostname)
}
}
}
@@ -389,7 +391,8 @@ func getSvcIpList(svcList []*v1.Service) []string {
func SortLbIngressList(lbi []v1.LoadBalancerIngress) func(int, int) bool {
return func(i int, j int) bool {
return lbi[i].IP < lbi[j].IP
return loadBalancerIngressAddress(lbi[i].IP, lbi[i].Hostname) <
loadBalancerIngressAddress(lbi[j].IP, lbi[j].Hostname)
}
}
@@ -397,7 +400,11 @@ func GetLbStatusList(svcList []*v1.Service) []v1.LoadBalancerIngress {
svcIpList := getSvcIpList(svcList)
lbi := make([]v1.LoadBalancerIngress, 0, len(svcIpList))
for _, ep := range svcIpList {
lbi = append(lbi, v1.LoadBalancerIngress{IP: ep})
if net.ParseIP(ep) != nil {
lbi = append(lbi, v1.LoadBalancerIngress{IP: ep})
} else {
lbi = append(lbi, v1.LoadBalancerIngress{Hostname: ep})
}
}
sort.SliceStable(lbi, SortLbIngressList(lbi))
@@ -406,7 +413,8 @@ func GetLbStatusList(svcList []*v1.Service) []v1.LoadBalancerIngress {
func SortLbIngressListV1(lbi []networkingv1.IngressLoadBalancerIngress) func(int, int) bool {
return func(i int, j int) bool {
return lbi[i].IP < lbi[j].IP
return loadBalancerIngressAddress(lbi[i].IP, lbi[i].Hostname) <
loadBalancerIngressAddress(lbi[j].IP, lbi[j].Hostname)
}
}
@@ -414,7 +422,11 @@ func GetLbStatusListV1(svcList []*v1.Service) []networkingv1.IngressLoadBalancer
svcIpList := getSvcIpList(svcList)
lbi := make([]networkingv1.IngressLoadBalancerIngress, 0, len(svcIpList))
for _, ep := range svcIpList {
lbi = append(lbi, networkingv1.IngressLoadBalancerIngress{IP: ep})
if net.ParseIP(ep) != nil {
lbi = append(lbi, networkingv1.IngressLoadBalancerIngress{IP: ep})
} else {
lbi = append(lbi, networkingv1.IngressLoadBalancerIngress{Hostname: ep})
}
}
sort.SliceStable(lbi, SortLbIngressListV1(lbi))
@@ -423,7 +435,8 @@ func GetLbStatusListV1(svcList []*v1.Service) []networkingv1.IngressLoadBalancer
func SortLbIngressListV1Beta1(lbi []networkingv1beta1.IngressLoadBalancerIngress) func(int, int) bool {
return func(i int, j int) bool {
return lbi[i].IP < lbi[j].IP
return loadBalancerIngressAddress(lbi[i].IP, lbi[i].Hostname) <
loadBalancerIngressAddress(lbi[j].IP, lbi[j].Hostname)
}
}
@@ -431,9 +444,20 @@ func GetLbStatusListV1Beta1(svcList []*v1.Service) []networkingv1beta1.IngressLo
svcIpList := getSvcIpList(svcList)
lbi := make([]networkingv1beta1.IngressLoadBalancerIngress, 0, len(svcIpList))
for _, ep := range svcIpList {
lbi = append(lbi, networkingv1beta1.IngressLoadBalancerIngress{IP: ep})
if net.ParseIP(ep) != nil {
lbi = append(lbi, networkingv1beta1.IngressLoadBalancerIngress{IP: ep})
} else {
lbi = append(lbi, networkingv1beta1.IngressLoadBalancerIngress{Hostname: ep})
}
}
sort.SliceStable(lbi, SortLbIngressListV1Beta1(lbi))
return lbi
}
func loadBalancerIngressAddress(ip, hostname string) string {
if ip != "" {
return ip
}
return hostname
}

View File

@@ -21,6 +21,8 @@ import (
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/alibaba/higress/v2/pkg/ingress/kube/annotations"
@@ -206,6 +208,9 @@ func TestGenerateUniqueRouteName(t *testing.T) {
func TestGetLbStatusList(t *testing.T) {
clusterPrefix = "gw-123-"
svcName := clusterPrefix
aliyunHostname := "higress.cn-hangzhou.alb.aliyuncs.com"
awsHostname := "k8s-kubeingr-higressg-1234567890.eu-north-1.elb.amazonaws.com"
tencentHostname := "lb-12345678.clb.ap-guangzhou.tencentclb.com"
svcList := []*v1.Service{
{
ObjectMeta: metav1.ObjectMeta{
@@ -224,6 +229,57 @@ func TestGetLbStatusList(t *testing.T) {
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: awsHostname,
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: aliyunHostname,
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: tencentHostname,
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
@@ -295,17 +351,15 @@ func TestGetLbStatusList(t *testing.T) {
}
lbiList := GetLbStatusList(svcList)
if len(lbiList) != 4 {
t.Fatal("len should be 4")
}
if lbiList[0].IP != "1.1.1.1" {
t.Fatal("should be 1.1.1.1")
}
if lbiList[3].IP != "4.4.4.4" {
t.Fatal("should be 4.4.4.4")
}
assert.Equal(t, []v1.LoadBalancerIngress{
{IP: "1.1.1.1"},
{IP: "2.2.2.2"},
{IP: "3.3.3.3"},
{IP: "4.4.4.4"},
{Hostname: aliyunHostname},
{Hostname: awsHostname},
{Hostname: tencentHostname},
}, lbiList)
}
func TestSortRoutes(t *testing.T) {
@@ -1012,6 +1066,9 @@ func TestPartMd5(t *testing.T) {
func TestGetLbStatusListV1AndV1Beta1(t *testing.T) {
clusterPrefix = "gw-123-"
svcName := clusterPrefix
aliyunHostname := "higress.cn-hangzhou.alb.aliyuncs.com"
awsHostname := "k8s-kubeingr-higressg-1234567890.eu-north-1.elb.amazonaws.com"
tencentHostname := "lb-12345678.clb.ap-guangzhou.tencentclb.com"
svcList := []*v1.Service{
{
ObjectMeta: metav1.ObjectMeta{
@@ -1047,23 +1104,82 @@ func TestGetLbStatusListV1AndV1Beta1(t *testing.T) {
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: awsHostname,
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: aliyunHostname,
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: tencentHostname,
},
},
},
},
},
}
// Test the V1 version
t.Run("GetLbStatusListV1", func(t *testing.T) {
lbiList := GetLbStatusListV1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
assert.Equal(t, []networkingv1.IngressLoadBalancerIngress{
{IP: "1.1.1.1"},
{IP: "2.2.2.2"},
{Hostname: aliyunHostname},
{Hostname: awsHostname},
{Hostname: tencentHostname},
}, lbiList)
})
// Test the V1Beta1 version
t.Run("GetLbStatusListV1Beta1", func(t *testing.T) {
lbiList := GetLbStatusListV1Beta1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
assert.Equal(t, []networkingv1beta1.IngressLoadBalancerIngress{
{IP: "1.1.1.1"},
{IP: "2.2.2.2"},
{Hostname: aliyunHostname},
{Hostname: awsHostname},
{Hostname: tencentHostname},
}, lbiList)
})
}

View File

@@ -77,6 +77,7 @@ func NewController(client kube.Client, options common.Options, xdsUpdater model.
ClusterID: clusterId,
Revision: higressconfig.Revision,
}
istiogateway.SetGatewayClassName(options.GatewayClass)
istioController := istiogateway.NewController(client, client.CrdWatcher().WaitForCRD, opt, xdsUpdater)
if options.GatewaySelectorKey != "" {
istioController.DefaultGatewaySelector = map[string]string{options.GatewaySelectorKey: options.GatewaySelectorValue}

View File

@@ -27,7 +27,6 @@ import (
gw "sigs.k8s.io/gateway-api/apis/v1"
gatewayx "sigs.k8s.io/gateway-api/apisx/v1alpha1"
higressconstants "github.com/alibaba/higress/v2/pkg/config/constants"
networking "istio.io/api/networking/v1alpha3"
networkingclient "istio.io/client-go/pkg/apis/networking/v1"
kubesecrets "istio.io/istio/pilot/pkg/credentials/kube"
@@ -425,7 +424,7 @@ func BackendTLSPolicyCollection(
Kind: ptr.Of(gw.Kind(gvk.KubernetesGateway.Kind)),
Name: gw.ObjectName(g.Name),
}
ancestorStatus = append(ancestorStatus, setAncestorStatus(pr, status, i.Generation, conds, gw.GatewayController(higressconstants.ManagedGatewayController)))
ancestorStatus = append(ancestorStatus, setAncestorStatus(pr, status, i.Generation, conds, gw.GatewayController(managedGatewayController)))
}
status.Ancestors = mergeAncestors(status.Ancestors, ancestorStatus)
return status, res
@@ -635,14 +634,16 @@ func parentRefEqual(a, b gw.ParentReference) bool {
ptr.Equal(a.Port, b.Port)
}
var outControllers = sets.New(gw.GatewayController(higressconstants.ManagedGatewayController), constants.ManagedGatewayMeshController)
func isOutController(controller gw.GatewayController) bool {
return controller == managedGatewayController || controller == constants.ManagedGatewayMeshController
}
// mergeAncestors merges an existing ancestor with in incoming one. We preserve order, prune stale references set by our controller,
// and add any new references from our controller.
func mergeAncestors(existing []gw.PolicyAncestorStatus, incoming []gw.PolicyAncestorStatus) []gw.PolicyAncestorStatus {
n := 0
for _, x := range existing {
if !outControllers.Contains(x.ControllerName) {
if !isOutController(x.ControllerName) {
// Keep it as-is
existing[n] = x
n++

View File

@@ -21,7 +21,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s "sigs.k8s.io/gateway-api/apis/v1"
higressconstants "github.com/alibaba/higress/v2/pkg/config/constants"
"istio.io/istio/pilot/pkg/features"
"istio.io/istio/pilot/pkg/model/kstatus"
"istio.io/istio/pkg/config/schema/gvk"
@@ -49,11 +48,12 @@ func createRouteStatus(
generation int64,
currentParents []k8s.RouteParentStatus,
) []k8s.RouteParentStatus {
controllerName := k8s.GatewayController(managedGatewayController)
parents := slices.Clone(currentParents)
parentIndexes := map[string]int{}
for idx, p := range parents {
// Only consider our own
if p.ControllerName != k8s.GatewayController(higressconstants.ManagedGatewayController) {
if p.ControllerName != controllerName {
continue
}
rs := parentRefString(p.ParentRef, objectNamespace)
@@ -186,14 +186,14 @@ func createRouteStatus(
var currentConditions []metav1.Condition
currentStatus := slices.FindFunc(currentParents, func(s k8s.RouteParentStatus) bool {
return parentRefString(s.ParentRef, objectNamespace) == myRef &&
s.ControllerName == k8s.GatewayController(higressconstants.ManagedGatewayController)
s.ControllerName == controllerName
})
if currentStatus != nil {
currentConditions = currentStatus.Conditions
}
ns := k8s.RouteParentStatus{
ParentRef: gw.OriginalReference,
ControllerName: k8s.GatewayController(higressconstants.ManagedGatewayController),
ControllerName: controllerName,
Conditions: setConditions(generation, currentConditions, conds),
}
// Parent ref already exists, insert in the same place

View File

@@ -27,6 +27,7 @@ import (
)
func TestCreateRouteStatus(t *testing.T) {
setGatewayClassNameForTest(t, "")
lastTransitionTime := metav1.Now()
parentRef := httpRouteSpec.ParentRefs[0]
parentStatus := []k8s.RouteParentStatus{
@@ -122,3 +123,29 @@ func TestCreateRouteStatus(t *testing.T) {
})
}
}
func TestCreateRouteStatusWithCustomController(t *testing.T) {
if runInGatewayClassSubprocess(t) {
return
}
setGatewayClassNameForTest(t, "higress-internal")
parentRef := httpRouteSpec.ParentRefs[0]
customController := k8s.GatewayController(managedGatewayController)
current := []k8s.RouteParentStatus{
{
ParentRef: parentRef,
ControllerName: k8s.GatewayController(higressconstants.ManagedGatewayController),
},
}
got := createRouteStatus([]RouteParentResult{{OriginalReference: parentRef}}, "default", 1, current)
if len(got) != 2 {
t.Fatalf("expected default and custom controller status entries, got %+v", got)
}
if got[0].ControllerName != k8s.GatewayController(higressconstants.ManagedGatewayController) {
t.Fatalf("expected existing default controller status to be preserved, got %+v", got)
}
if got[1].ControllerName != customController {
t.Fatalf("expected custom controller status %q, got %+v", customController, got)
}
}

View File

@@ -15,8 +15,13 @@
package istio
import (
"os"
"os/exec"
"regexp"
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -27,6 +32,7 @@ import (
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/networking/core"
"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
"istio.io/istio/pilot/pkg/status"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/constants"
"istio.io/istio/pkg/config/schema/gvk"
@@ -77,6 +83,7 @@ var AlwaysReady = func(class schema.GroupVersionResource, stop <-chan struct{})
}
func setupController(t *testing.T, objs ...runtime.Object) *Controller {
setGatewayClassNameForTest(t, "")
kc := kube.NewFakeClient(objs...)
setupClientCRDs(t, kc)
stop := test.NewStop(t)
@@ -94,6 +101,57 @@ func setupController(t *testing.T, objs ...runtime.Object) *Controller {
return controller
}
func setupControllerWithGatewayClass(t *testing.T, gatewayClass string, objs ...runtime.Object) *Controller {
setGatewayClassNameForTest(t, gatewayClass)
kc := kube.NewFakeClient(objs...)
setupClientCRDs(t, kc)
stop := test.NewStop(t)
controller := NewController(
kc,
AlwaysReady,
controller.Options{KrtDebugger: krt.GlobalDebugHandler},
nil)
kc.RunAndWait(stop)
go controller.Run(stop)
cg := core.NewConfigGenTest(t, core.TestOptions{})
controller.Reconcile(cg.PushContext())
kube.WaitForCacheSync("test", stop, controller.HasSynced)
return controller
}
func setGatewayClassNameForTest(t *testing.T, gatewayClass string) {
t.Helper()
if gatewayClass != "" {
SetGatewayClassName(gatewayClass)
}
}
func runInGatewayClassSubprocess(t *testing.T) bool {
t.Helper()
const env = "HIGRESS_TEST_GATEWAY_CLASS_SUBPROCESS"
if os.Getenv(env) == t.Name() {
return false
}
cmd := exec.Command(os.Args[0], "-test.run=^"+regexp.QuoteMeta(t.Name())+"$", "-test.count=1")
cmd.Env = append(testEnvWithoutCoverage(), env+"="+t.Name())
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("gateway class subprocess failed: %v\n%s", err, out)
}
return true
}
func testEnvWithoutCoverage() []string {
var out []string
for _, kv := range os.Environ() {
if strings.HasPrefix(kv, "GOCOVERDIR=") {
continue
}
out = append(out, kv)
}
return out
}
func TestListInvalidGroupVersionKind(t *testing.T) {
controller := setupController(t)
@@ -135,3 +193,136 @@ func TestListGatewayResourceType(t *testing.T) {
assert.Equal(t, c.Spec, any(expectedgw))
}
}
func TestListGatewayResourceTypeWithAlternateGatewayClassName(t *testing.T) {
alternateGateway := gatewaySpec.DeepCopy()
alternateGateway.GatewayClassName = "alternate"
controller := setupController(t,
&k8sbeta.GatewayClass{
ObjectMeta: metav1.ObjectMeta{Name: "alternate"},
Spec: *gatewayClassSpec,
},
&k8sbeta.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "alternate-gw", Namespace: "ns1"},
Spec: *alternateGateway,
})
cfg := controller.List(gvk.Gateway, "ns1")
assert.Equal(t, len(cfg), 1)
assert.Equal(t, cfg[0].Name, "alternate-gw-"+constants.KubernetesGatewayName+"-default")
}
func TestHTTPRouteBackendServiceLifecycle(t *testing.T) {
backendPort := k8s.PortNumber(80)
route := httpRouteSpec.DeepCopy()
route.Rules = []k8s.HTTPRouteRule{{
BackendRefs: []k8s.HTTPBackendRef{{
BackendRef: k8s.BackendRef{
BackendObjectReference: k8s.BackendObjectReference{
Name: "backend",
Port: &backendPort,
},
},
}},
}}
controller := setupController(t,
&k8sbeta.GatewayClass{
ObjectMeta: metav1.ObjectMeta{Name: "higress"},
Spec: *gatewayClassSpec,
},
&k8sbeta.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gwspec", Namespace: "ns1"},
Spec: *gatewaySpec,
},
&k8sbeta.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{Name: "http-route", Namespace: "ns1"},
Spec: *route,
},
)
statusQueue := &TestStatusQueue{state: map[status.Resource]any{}}
setAndWait(t, controller, statusQueue)
resolvedRefs := func() string {
for _, rawStatus := range statusQueue.Statuses() {
routeStatus, ok := rawStatus.(*k8s.HTTPRouteStatus)
if !ok {
continue
}
for _, parent := range routeStatus.Parents {
for _, condition := range parent.Conditions {
if condition.Type == string(k8s.RouteConditionResolvedRefs) {
return string(condition.Status) + "/" + condition.Reason
}
}
}
}
return ""
}
assert.EventuallyEqual(t, resolvedRefs, "False/BackendNotFound")
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "backend", Namespace: "ns1"},
Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{
Name: "http",
Port: int32(backendPort),
}}},
}
if _, err := controller.client.Kube().CoreV1().Services("ns1").Create(t.Context(), service, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
assert.EventuallyEqual(t, resolvedRefs, "True/ResolvedRefs")
if err := controller.client.Kube().CoreV1().Services("ns1").Delete(t.Context(), service.Name, metav1.DeleteOptions{}); err != nil {
t.Fatal(err)
}
assert.EventuallyEqual(t, resolvedRefs, "False/BackendNotFound")
}
func TestListGatewayResourceTypeWithCustomGatewayClass(t *testing.T) {
if runInGatewayClassSubprocess(t) {
return
}
customGatewayClass := "higress-internal"
customControllerName := higressconstant.ManagedGatewayController + "-" + customGatewayClass
defaultGateway := gatewaySpec.DeepCopy()
defaultGateway.GatewayClassName = k8s.ObjectName(higressconstant.DefaultGatewayClass)
customGateway := gatewaySpec.DeepCopy()
customGateway.GatewayClassName = k8s.ObjectName(customGatewayClass)
controller := setupControllerWithGatewayClass(t, customGatewayClass,
&k8sbeta.GatewayClass{
ObjectMeta: metav1.ObjectMeta{
Name: higressconstant.DefaultGatewayClass,
},
Spec: *gatewayClassSpec,
},
&k8sbeta.GatewayClass{
ObjectMeta: metav1.ObjectMeta{
Name: customGatewayClass,
},
Spec: k8s.GatewayClassSpec{
ControllerName: k8s.GatewayController(customControllerName),
},
},
&k8sbeta.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "default-gw",
Namespace: "ns1",
},
Spec: *defaultGateway,
},
&k8sbeta.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "custom-gw",
Namespace: "ns1",
},
Spec: *customGateway,
})
dumpOnFailure(t, krt.GlobalDebugHandler)
cfg := controller.List(gvk.Gateway, "ns1")
assert.Equal(t, len(cfg), 1)
assert.Equal(t, cfg[0].Name, "custom-gw"+"-"+constants.KubernetesGatewayName+"-default")
assert.Equal(t, cfg[0].Namespace, "ns1")
assert.Equal(t, cfg[0].Spec, any(expectedgw))
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/alibaba/higress/v2/pkg/ingress/kube/util"
"istio.io/istio/pilot/pkg/credentials"
"net"
"net/netip"
"path"
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
"sort"
@@ -1063,12 +1064,12 @@ func buildDestination(ctx RouteContext, to k8s.BackendRef, ns string,
return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "service name invalid; the name of the Service must be used, not the hostname."}
}
hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.DomainSuffix)
// Start - Updated by Higress
//key := namespace + "/" + string(to.Name)
//svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key)))
svc := ctx.LookupHostname(hostname, namespace, "Service")
// End - Updated by Higress
if svc == nil {
// Keep Higress service resolution as the primary lookup so its service naming and
// plugin binding semantics remain unchanged. Always fetch from the informer to
// retain the dependency needed to recompute the Route when the Service is deleted.
key := namespace + "/" + string(to.Name)
kubeSvc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key)))
if ctx.LookupHostname(hostname, namespace, "Service") == nil && kubeSvc == nil {
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
}
case config.GroupVersionKind{Group: gvk.ServiceEntry.Group, Kind: "Hostname"}:
@@ -1591,6 +1592,21 @@ func (r routeParentReference) IsMesh() bool {
return r.InternalName == "mesh"
}
func (r routeParentReference) hostnameIntersection(rawRouteHost string) (string, bool) {
routeHost := host.Name(rawRouteHost)
listenerHost := host.Name(r.Hostname)
if len(listenerHost) == 0 || listenerHost == "*" {
return rawRouteHost, true
}
if routeHost.SubsetOf(listenerHost) {
return rawRouteHost, true
}
if listenerHost.SubsetOf(routeHost) {
return r.Hostname, true
}
return "", false
}
func (r routeParentReference) hostnameAllowedByIsolation(rawRouteHost string) bool {
routeHost := host.Name(rawRouteHost)
ourListener := host.Name(r.Hostname)
@@ -1735,9 +1751,7 @@ func reportGatewayStatus(
setProgrammedCondition(gatewayConditions, internal, gatewayServices, warnings, allUsable)
addressesToReport := external
addrType := k8s.IPAddressType
if len(addressesToReport) == 0 {
addrType = k8s.HostnameAddressType
for _, hostport := range internal {
svchost, _, _ := net.SplitHostPort(hostport)
if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) {
@@ -1745,12 +1759,21 @@ func reportGatewayStatus(
}
}
}
gs.Addresses = make([]k8s.GatewayStatusAddress, 0, len(addressesToReport))
for _, addr := range addressesToReport {
gs.Addresses = append(gs.Addresses, k8s.GatewayStatusAddress{
Value: addr,
Type: &addrType,
})
// Do not report an address until we are ready. But once we are ready, never remove the address.
if len(addressesToReport) > 0 {
gs.Addresses = make([]k8s.GatewayStatusAddress, 0, len(addressesToReport))
for _, addr := range addressesToReport {
var addrType k8s.AddressType
if _, err := netip.ParseAddr(addr); err == nil {
addrType = k8s.IPAddressType
} else {
addrType = k8s.HostnameAddressType
}
gs.Addresses = append(gs.Addresses, k8s.GatewayStatusAddress{
Value: addr,
Type: &addrType,
})
}
}
// Prune listeners that have been removed
haveListeners := getListenerNames(&obj.Spec)

View File

@@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
k8s "sigs.k8s.io/gateway-api/apis/v1"
k8sbeta "sigs.k8s.io/gateway-api/apis/v1beta1"
"sigs.k8s.io/gateway-api/pkg/consts"
"sigs.k8s.io/yaml"
@@ -130,9 +131,9 @@ var services = []*model.Service{
Attributes: model.ServiceAttributes{
Namespace: "default",
Labels: map[string]string{
"higress.io/inferencepool-extension-service": "ext-proc-svc",
"higress.io/inferencepool-extension-port": "9002",
"higress.io/inferencepool-extension-failure-mode": "FailClose",
InferencePoolExtensionRefSvc: "ext-proc-svc",
InferencePoolExtensionRefPort: "9002",
InferencePoolExtensionRefFailureMode: "FailClose",
},
},
Ports: ports,
@@ -145,9 +146,9 @@ var services = []*model.Service{
Attributes: model.ServiceAttributes{
Namespace: "default",
Labels: map[string]string{
"higress.io/inferencepool-extension-service": "ext-proc-svc-2",
"higress.io/inferencepool-extension-port": "9002",
"higress.io/inferencepool-extension-failure-mode": "FailClose",
InferencePoolExtensionRefSvc: "ext-proc-svc-2",
InferencePoolExtensionRefPort: "9002",
InferencePoolExtensionRefFailureMode: "FailClose",
},
},
Ports: ports,
@@ -156,6 +157,36 @@ var services = []*model.Service{
return name
}())),
},
{
Attributes: model.ServiceAttributes{
Namespace: "default",
Labels: map[string]string{
InferencePoolExtensionRefSvc: "model1-epp",
InferencePoolExtensionRefPort: "9002",
InferencePoolExtensionRefFailureMode: "FailClose",
},
},
Ports: ports,
Hostname: host.Name(fmt.Sprintf("%s.default.svc.domain.suffix", func() string {
name, _ := InferencePoolServiceName("infpool-model1")
return name
}())),
},
{
Attributes: model.ServiceAttributes{
Namespace: "default",
Labels: map[string]string{
InferencePoolExtensionRefSvc: "model2-epp",
InferencePoolExtensionRefPort: "9002",
InferencePoolExtensionRefFailureMode: "FailClose",
},
},
Ports: ports,
Hostname: host.Name(fmt.Sprintf("%s.default.svc.domain.suffix", func() string {
name, _ := InferencePoolServiceName("infpool-model2")
return name
}())),
},
{
Attributes: model.ServiceAttributes{
@@ -570,9 +601,8 @@ func init() {
features.EnableAlphaGatewayAPI = true
features.EnableAmbientWaypoints = true
features.EnableAmbientMultiNetwork = true
// Recompute with ambient enabled
classInfos = getClassInfos()
builtinClasses = getBuiltinClasses()
// Recompute with the desired feature flags.
SetGatewayClassName("")
}
type TestStatusQueue struct {
@@ -794,6 +824,128 @@ func TestConvertResources(t *testing.T) {
}
}
func TestReportGatewayStatusAddressType(t *testing.T) {
cases := []struct {
name string
ingresses []corev1.LoadBalancerIngress
want map[string]k8s.AddressType
}{
{
name: "load balancer IPv4",
ingresses: []corev1.LoadBalancerIngress{
{IP: "47.98.1.2"},
},
want: map[string]k8s.AddressType{
"47.98.1.2": k8s.IPAddressType,
},
},
{
name: "load balancer IPv6",
ingresses: []corev1.LoadBalancerIngress{
{IP: "2001:db8::1"},
},
want: map[string]k8s.AddressType{
"2001:db8::1": k8s.IPAddressType,
},
},
{
name: "load balancer hostname",
ingresses: []corev1.LoadBalancerIngress{
{Hostname: "k8s-higress-higressg-1234567890.us-west-2.elb.amazonaws.com"},
},
want: map[string]k8s.AddressType{
"k8s-higress-higressg-1234567890.us-west-2.elb.amazonaws.com": k8s.HostnameAddressType,
},
},
{
name: "mixed load balancer addresses",
ingresses: []corev1.LoadBalancerIngress{
{IP: "47.98.1.2"},
{Hostname: "higress.cn-hangzhou.alb.aliyuncs.com"},
},
want: map[string]k8s.AddressType{
"47.98.1.2": k8s.IPAddressType,
"higress.cn-hangzhou.alb.aliyuncs.com": k8s.HostnameAddressType,
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "higress-gateway",
Namespace: "higress-system",
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
Ports: []corev1.ServicePort{
{
Name: "http",
Port: 80,
Protocol: corev1.ProtocolTCP,
},
},
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: tt.ingresses,
},
},
}
stop := test.NewStop(t)
kc := kube.NewFakeClient(svc)
kc.RunAndWait(stop)
ctx := NewGatewayContext(nil, constants.DefaultClusterName, kc, "cluster.local")
gw := &k8sbeta.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "higress-gateway",
Namespace: "higress-system",
Generation: 1,
},
Spec: k8sbeta.GatewaySpec{
GatewayClassName: "higress",
Listeners: []k8sbeta.Listener{
{
Name: "http",
Port: 80,
Protocol: k8s.HTTPProtocolType,
},
},
},
}
gs := &k8sbeta.GatewayStatus{}
servers := []*istio.Server{
{
Port: &istio.Port{
Name: "http",
Number: 80,
Protocol: "HTTP",
},
},
}
reportGatewayStatus(&ctx, gw, gs, []string{"higress-gateway.higress-system.svc.cluster.local"}, servers, 0, nil)
if len(gs.Addresses) != len(tt.want) {
t.Fatalf("expected %d addresses, got %d: %#v", len(tt.want), len(gs.Addresses), gs.Addresses)
}
for _, got := range gs.Addresses {
wantType, ok := tt.want[got.Value]
if !ok {
t.Fatalf("unexpected address value %q in %#v", got.Value, gs.Addresses)
}
if got.Type == nil {
t.Fatalf("expected address %q type %q, got nil", got.Value, wantType)
}
if *got.Type != wantType {
t.Fatalf("expected address %q type %q, got %q", got.Value, wantType, *got.Type)
}
}
})
}
}
func setupClientCRDs(t *testing.T, kc kube.CLIClient) {
for _, crd := range []schema.GroupVersionResource{
gvr.KubernetesGateway,
@@ -1370,6 +1522,36 @@ func TestGatewayReferenceAllowedParentHostnameParsing(t *testing.T) {
}
}
func TestRouteParentReferenceHostnameIntersection(t *testing.T) {
tests := []struct {
name string
listenerHost string
routeHost string
wantHost string
wantMatch bool
}{
{name: "empty listener", listenerHost: "", routeHost: "example.com", wantHost: "example.com", wantMatch: true},
{name: "empty route", listenerHost: "example.com", routeHost: "*", wantHost: "example.com", wantMatch: true},
{name: "exact match", listenerHost: "example.com", routeHost: "example.com", wantHost: "example.com", wantMatch: true},
{name: "exact listener and wildcard route", listenerHost: "foo.example.com", routeHost: "*.example.com", wantHost: "foo.example.com", wantMatch: true},
{name: "wildcard listener and exact route", listenerHost: "*.example.com", routeHost: "foo.example.com", wantHost: "foo.example.com", wantMatch: true},
{name: "narrower route wildcard", listenerHost: "*.example.com", routeHost: "*.foo.example.com", wantHost: "*.foo.example.com", wantMatch: true},
{name: "narrower listener wildcard", listenerHost: "*.foo.example.com", routeHost: "*.example.com", wantHost: "*.foo.example.com", wantMatch: true},
{name: "disjoint exact hosts", listenerHost: "foo.example.com", routeHost: "bar.example.com", wantMatch: false},
{name: "disjoint wildcard hosts", listenerHost: "*.example.com", routeHost: "*.example.net", wantMatch: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parent := routeParentReference{Hostname: tt.listenerHost}
gotHost, gotMatch := parent.hostnameIntersection(tt.routeHost)
if gotHost != tt.wantHost || gotMatch != tt.wantMatch {
t.Fatalf("hostnameIntersection() = (%q, %v), want (%q, %v)", gotHost, gotMatch, tt.wantHost, tt.wantMatch)
}
})
}
}
func TestReferencePolicy(t *testing.T) {
validator := crdvalidation.NewIstioValidator(t)
type res struct {

View File

@@ -48,15 +48,33 @@ type classInfo struct {
addressType gateway.AddressType
}
var classInfos = getClassInfos()
var (
gatewayClassName = gateway.ObjectName(higressconstants.DefaultGatewayClass)
managedGatewayController = gateway.GatewayController(higressconstants.ManagedGatewayController)
classInfos = getClassInfos()
builtinClasses = getBuiltinClasses()
)
var builtinClasses = getBuiltinClasses()
// SetGatewayClassName configures the single GatewayClassName this process owns.
func SetGatewayClassName(gatewayClass string) {
if gatewayClass == "" {
gatewayClass = higressconstants.DefaultGatewayClass
}
gatewayClassName = gateway.ObjectName(gatewayClass)
if gatewayClass == higressconstants.DefaultGatewayClass {
managedGatewayController = gateway.GatewayController(higressconstants.ManagedGatewayController)
} else {
managedGatewayController = gateway.GatewayController(higressconstants.ManagedGatewayController + "-" + gatewayClass)
}
classInfos = getClassInfos()
builtinClasses = getBuiltinClasses()
}
func getBuiltinClasses() map[gateway.ObjectName]gateway.GatewayController {
res := map[gateway.ObjectName]gateway.GatewayController{
// Start - Updated by Higress
//gateway.ObjectName(features.GatewayAPIDefaultGatewayClass): gateway.GatewayController(features.ManagedGatewayController),
higressconstants.DefaultGatewayClass: higressconstants.ManagedGatewayController,
gatewayClassName: managedGatewayController,
// End - Updated by Higress
}
// Start - Commented by Higress
@@ -80,8 +98,8 @@ func getBuiltinClasses() map[gateway.ObjectName]gateway.GatewayController {
func getClassInfos() map[gateway.GatewayController]classInfo {
// Start - Updated by Higress
m := map[gateway.GatewayController]classInfo{
gateway.GatewayController(higressconstants.ManagedGatewayController): {
controller: higressconstants.ManagedGatewayController,
managedGatewayController: {
controller: string(managedGatewayController),
description: "The default Higress GatewayClass",
templates: "kube-gateway",
defaultServiceType: corev1.ServiceTypeLoadBalancer,

View File

@@ -38,8 +38,10 @@ func GatewayClassesCollection(
krt.Collection[GatewayClass],
) {
return krt.NewStatusCollection(gatewayClasses, func(ctx krt.HandlerContext, obj *gateway.GatewayClass) (*gateway.GatewayClassStatus, *GatewayClass) {
_, known := classInfos[obj.Spec.ControllerName]
if !known {
if obj.Spec.ControllerName != managedGatewayController {
return nil, nil
}
if _, known := classInfos[obj.Spec.ControllerName]; !known {
return nil, nil
}
status := obj.Status.DeepCopy()

View File

@@ -16,13 +16,13 @@ package istio
import (
"fmt"
"github.com/alibaba/higress/v2/pkg/config/constants"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gateway "sigs.k8s.io/gateway-api/apis/v1beta1"
"github.com/alibaba/higress/v2/pkg/config/constants"
"istio.io/istio/pkg/kube"
"istio.io/istio/pkg/kube/kclient/clienttest"
"istio.io/istio/pkg/test"
@@ -30,6 +30,7 @@ import (
)
func TestClassController(t *testing.T) {
setGatewayClassNameForTest(t, "")
client := kube.NewFakeClient()
cc := NewClassController(client)
classes := clienttest.Wrap(t, cc.classes)
@@ -91,3 +92,70 @@ func TestClassController(t *testing.T) {
deleteClass("something-else")
expectClass("something-else", "")
}
func TestClassControllerWithCustomGatewayClass(t *testing.T) {
if runInGatewayClassSubprocess(t) {
return
}
gatewayClass := "higress-internal"
setGatewayClassNameForTest(t, gatewayClass)
client := kube.NewFakeClient()
controllerName := string(gateway.GatewayController(constants.ManagedGatewayController + "-" + gatewayClass))
cc := NewClassController(client)
classes := clienttest.Wrap(t, cc.classes)
stop := test.NewStop(t)
client.RunAndWait(stop)
go cc.Run(stop)
expectClass := func(name, controller string) {
t.Helper()
retry.UntilSuccessOrFail(t, func() error {
gc := classes.Get(name, "")
if controller == "" {
if gc == nil {
return nil
}
return fmt.Errorf("expected no class, got %v", gc.Spec.ControllerName)
}
if gc == nil {
return fmt.Errorf("expected class %v, got none", controller)
}
if gateway.GatewayController(controller) != gc.Spec.ControllerName {
return fmt.Errorf("expected class %v, got %v", controller, gc.Spec.ControllerName)
}
return nil
}, retry.Timeout(time.Second*3))
}
expectClass(gatewayClass, controllerName)
expectClass(constants.DefaultGatewayClass, "")
}
func TestSetGatewayClassName(t *testing.T) {
if runInGatewayClassSubprocess(t) {
return
}
SetGatewayClassName("")
if gatewayClassName != gateway.ObjectName(constants.DefaultGatewayClass) {
t.Fatalf("expected default gateway class %q, got %q", constants.DefaultGatewayClass, gatewayClassName)
}
if managedGatewayController != gateway.GatewayController(constants.ManagedGatewayController) {
t.Fatalf("expected default controller %q, got %q", constants.ManagedGatewayController, managedGatewayController)
}
customClass := "higress-internal"
SetGatewayClassName(customClass)
customController := gateway.GatewayController(constants.ManagedGatewayController + "-" + customClass)
if gatewayClassName != gateway.ObjectName(customClass) {
t.Fatalf("expected custom gateway class %q, got %q", customClass, gatewayClassName)
}
if managedGatewayController != customController {
t.Fatalf("expected custom controller %q, got %q", customController, managedGatewayController)
}
if got := builtinClasses[gateway.ObjectName(customClass)]; got != customController {
t.Fatalf("expected builtin class controller %q, got %q", customController, got)
}
if _, exists := builtinClasses[gateway.ObjectName(constants.DefaultGatewayClass)]; exists {
t.Fatalf("custom config should not include default gateway class %q", constants.DefaultGatewayClass)
}
}

View File

@@ -52,8 +52,6 @@ const (
// ControllerName is the name of this controller for labeling resources it manages
const ControllerName = "inference-controller"
var supportedControllers = getSupportedControllers()
func getSupportedControllers() sets.Set[gatewayv1.GatewayController] {
ret := sets.New[gatewayv1.GatewayController]()
for _, controller := range builtinClasses {
@@ -241,7 +239,7 @@ func findGatewayParents(
for _, parentStatus := range route.Status.Parents {
// Only consider parents managed by our supported controllers (from supportedControllers variable)
// This filters out parents from other controllers we don't manage
if !supportedControllers.Contains(parentStatus.ControllerName) {
if !getSupportedControllers().Contains(parentStatus.ControllerName) {
continue
}
@@ -354,7 +352,7 @@ func calculateAcceptedStatus(
// Check if this route has our gateway as a parent and if it's accepted
for _, parentStatus := range route.Status.Parents {
// Only consider parents managed by supported controllers
if !supportedControllers.Contains(parentStatus.ControllerName) {
if !getSupportedControllers().Contains(parentStatus.ControllerName) {
continue
}

View File

@@ -148,7 +148,12 @@ func HTTPRouteCollection(
}
// Create one VS per hostname with a single hostname.
// This ensures we can treat each hostname independently, as the spec requires
for _, h := range vsHosts {
processedHosts := sets.New[string]()
for _, routeHost := range vsHosts {
h, ok := parent.hostnameIntersection(routeHost)
if !ok || processedHosts.InsertContains(h) {
continue
}
if !parent.hostnameAllowedByIsolation(h) {
// TODO: standardize a status message for this upstream and report
continue
@@ -766,12 +771,63 @@ func mergeHTTPRoutes(baseVirtualServices krt.Collection[RouteWithKey], opts ...k
sortRoutesByCreationTime(configs)
base := configs[0].DeepCopy()
baseVS := base.Spec.(*istio.VirtualService)
for _, config := range configs[1:] {
// Deep copy the InferencePool configs map to avoid race conditions
// The default DeepCopy() only does shallow copy of Extra field
if base.Extra != nil {
if ipConfigs, ok := base.Extra[constants.ConfigExtraPerRouteRuleInferencePoolConfigs].(map[string]kube.InferencePoolRouteRuleConfig); ok {
// Create a new map to avoid modifying the shared underlying map
newIPConfigs := make(map[string]kube.InferencePoolRouteRuleConfig, len(ipConfigs))
for k, v := range ipConfigs {
newIPConfigs[k] = v
}
base.Extra[constants.ConfigExtraPerRouteRuleInferencePoolConfigs] = newIPConfigs
}
}
for i, config := range configs[1:] {
thisVS := config.Spec.(*istio.VirtualService)
baseVS.Http = append(baseVS.Http, thisVS.Http...)
// append parents
base.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s",
base.Annotations[constants.InternalParentNames], config.Annotations[constants.InternalParentNames])
// Merge Extra field (especially for InferencePool configs)
if base.Extra == nil && config.Extra != nil {
base.Extra = make(map[string]any)
}
if config.Extra != nil {
for k, v := range config.Extra {
// For non-InferencePool configs, keep the first value for stability
if k != constants.ConfigExtraPerRouteRuleInferencePoolConfigs {
if _, exists := base.Extra[k]; !exists {
base.Extra[k] = v
}
continue
}
// For InferencePool configs, merge the maps
baseMap, baseOk := base.Extra[k].(map[string]kube.InferencePoolRouteRuleConfig)
configMap, configOk := v.(map[string]kube.InferencePoolRouteRuleConfig)
if baseOk && configOk {
log.Debugf("Merging InferencePool configs: adding %d route configs from VirtualService %d to base (namespace=%s)",
len(configMap), i+1, config.Namespace)
// Route names are composed of the HTTPRoute/VirtualService namespaced name so they can't possibly conflict
for routeName, routeConfig := range configMap {
baseMap[routeName] = routeConfig
}
} else if configOk {
if _, exists := base.Extra[k]; !exists {
log.Debugf("Creating new InferencePool config map from VirtualService %d (namespace=%s)", i+1, config.Namespace)
base.Extra[k] = v
}
} else if !configOk {
log.Debugf("Skipping InferencePool config from VirtualService %d due to unexpected type (namespace=%s)", i+1, config.Namespace)
}
}
}
}
// Log final merged InferencePool configs
if base.Extra != nil {
if ipConfigs, ok := base.Extra[constants.ConfigExtraPerRouteRuleInferencePoolConfigs].(map[string]kube.InferencePoolRouteRuleConfig); ok {
log.Debugf("Final merged VirtualService for key %s has %d InferencePool route configs", object.Key, len(ipConfigs))
}
}
sortHTTPRoutes(baseVS.Http)
base.Name = strings.ReplaceAll(object.Key, "/", "~")

View File

@@ -0,0 +1,140 @@
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package istio
import (
"strings"
"testing"
"time"
istio "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/constants"
"istio.io/istio/pkg/config/gateway/kube"
"istio.io/istio/pkg/kube/krt"
"istio.io/istio/pkg/test"
)
func TestMergeHTTPRoutesMergesInferencePoolExtra(t *testing.T) {
stop := test.NewStop(t)
routeKey := "default/gateway/example.com"
baseRouteName := "default/local-ai-chat"
otherRouteName := "default/local-ai-chat-360m"
baseInferenceConfigs := map[string]kube.InferencePoolRouteRuleConfig{
baseRouteName: {
FQDN: "local-ai-chat-pool-epp.default.svc.cluster.local",
Port: "9002",
FailureModeAllow: true,
},
}
otherInferenceConfigs := map[string]kube.InferencePoolRouteRuleConfig{
otherRouteName: {
FQDN: "local-ai-chat-360m-pool-epp.default.svc.cluster.local",
Port: "9002",
},
}
baseCfg := &config.Config{
Meta: config.Meta{
Name: "local-ai-chat",
Namespace: "default",
CreationTimestamp: time.Unix(1, 0),
Annotations: map[string]string{
constants.InternalParentNames: "parent-a",
},
},
Spec: &istio.VirtualService{
Hosts: []string{"example.com"},
Gateways: []string{"default/gateway"},
Http: []*istio.HTTPRoute{{
Name: baseRouteName,
}},
},
Extra: map[string]any{
constants.ConfigExtraPerRouteRuleInferencePoolConfigs: baseInferenceConfigs,
"non-inference-extra": "kept-from-base",
},
}
otherCfg := &config.Config{
Meta: config.Meta{
Name: "local-ai-chat-360m",
Namespace: "default",
CreationTimestamp: time.Unix(2, 0),
Annotations: map[string]string{
constants.InternalParentNames: "parent-b",
},
},
Spec: &istio.VirtualService{
Hosts: []string{"example.com"},
Gateways: []string{"default/gateway"},
Http: []*istio.HTTPRoute{{
Name: otherRouteName,
}},
},
Extra: map[string]any{
constants.ConfigExtraPerRouteRuleInferencePoolConfigs: otherInferenceConfigs,
"non-inference-extra": "ignored-from-later-route",
"other-extra": "added-from-later-route",
},
}
baseVirtualServices := krt.NewStaticCollection[RouteWithKey](nil, []RouteWithKey{
{
Config: baseCfg,
Key: routeKey,
},
{
Config: otherCfg,
Key: routeKey,
},
}, krt.WithStop(stop), krt.WithName("base"))
merged := mergeHTTPRoutes(baseVirtualServices, krt.WithStop(stop), krt.WithName("merged"))
merged.WaitUntilSynced(stop)
gotList := merged.List()
if len(gotList) != 1 {
t.Fatalf("expected one merged VirtualService, got %d", len(gotList))
}
got := gotList[0]
if got.Name != strings.ReplaceAll(routeKey, "/", "~") {
t.Fatalf("expected merged VirtualService name %q, got %q", strings.ReplaceAll(routeKey, "/", "~"), got.Name)
}
gotVS := got.Spec.(*istio.VirtualService)
if len(gotVS.Http) != 2 {
t.Fatalf("expected merged VirtualService to contain 2 HTTP routes, got %d", len(gotVS.Http))
}
gotInferenceConfigs, ok := got.Extra[constants.ConfigExtraPerRouteRuleInferencePoolConfigs].(map[string]kube.InferencePoolRouteRuleConfig)
if !ok {
t.Fatalf("expected merged InferencePool configs, got %T", got.Extra[constants.ConfigExtraPerRouteRuleInferencePoolConfigs])
}
if len(gotInferenceConfigs) != 2 {
t.Fatalf("expected 2 merged InferencePool configs, got %d: %v", len(gotInferenceConfigs), gotInferenceConfigs)
}
if gotInferenceConfigs[baseRouteName].FQDN != baseInferenceConfigs[baseRouteName].FQDN {
t.Fatalf("expected base route InferencePool config to be preserved, got %v", gotInferenceConfigs[baseRouteName])
}
if gotInferenceConfigs[otherRouteName].FQDN != otherInferenceConfigs[otherRouteName].FQDN {
t.Fatalf("expected later route InferencePool config to be merged, got %v", gotInferenceConfigs[otherRouteName])
}
if got.Extra["non-inference-extra"] != "kept-from-base" {
t.Fatalf("expected non-InferencePool Extra to keep base value, got %v", got.Extra["non-inference-extra"])
}
if got.Extra["other-extra"] != "added-from-later-route" {
t.Fatalf("expected missing non-InferencePool Extra to be added, got %v", got.Extra["other-extra"])
}
if _, found := baseInferenceConfigs[otherRouteName]; found {
t.Fatalf("expected base InferencePool config map not to be mutated by merge")
}
}

View File

@@ -23,13 +23,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/http.higress-system
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: higress-system
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- name: http
route:

View File

@@ -42,13 +42,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/http.apple
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-apple~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-apple~apple.example
namespace: apple
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-apple
hosts:
- '*'
- apple.example
http:
- name: apple/http
route:
@@ -63,13 +63,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/http.banana
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-banana~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-banana~banana.example
namespace: banana
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-banana
hosts:
- '*'
- banana.example
http:
- name: banana/http
route:

View File

@@ -14,6 +14,22 @@ metadata:
spec: null
status: {}
---
apiVersion: inference.networking.k8s.io/v1
kind: InferencePool
metadata:
name: infpool-model1
namespace: default
spec: null
status: {}
---
apiVersion: inference.networking.k8s.io/v1
kind: InferencePool
metadata:
name: infpool-model2
namespace: default
spec: null
status: {}
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: GatewayClass
metadata:
@@ -49,7 +65,7 @@ status:
status: "True"
type: Programmed
listeners:
- attachedRoutes: 11
- attachedRoutes: 13
conditions:
- lastTransitionTime: fake
message: No errors found
@@ -272,6 +288,54 @@ status:
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: multi-route-infpool-1
namespace: default
spec: null
status:
parents:
- conditions:
- lastTransitionTime: fake
message: Route was valid
reason: Accepted
status: "True"
type: Accepted
- lastTransitionTime: fake
message: All references resolved
reason: ResolvedRefs
status: "True"
type: ResolvedRefs
controllerName: higress.io/gateway-controller
parentRef:
name: gateway
namespace: higress-system
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: multi-route-infpool-2
namespace: default
spec: null
status:
parents:
- conditions:
- lastTransitionTime: fake
message: Route was valid
reason: Accepted
status: "True"
type: Accepted
- lastTransitionTime: fake
message: All references resolved
reason: ResolvedRefs
status: "True"
type: ResolvedRefs
controllerName: higress.io/gateway-controller
parentRef:
name: gateway
namespace: higress-system
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: multiple-inferencepool-backend-refs
namespace: default

View File

@@ -421,3 +421,80 @@ spec:
name: vllm-llama3-8b-instruct-epp
port:
number: 9002
---
# Test case for multiple HTTPRoutes with InferencePools on same gateway.
# This verifies that InferencePool configs in Config.Extra are preserved when
# the routes are merged into a single VirtualService.
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: multi-route-infpool-1
namespace: default
spec:
parentRefs:
- name: gateway
namespace: higress-system
hostnames: ["multi-infpool.domain.example"]
rules:
- matches:
- path:
type: PathPrefix
value: /model1
backendRefs:
- name: infpool-model1
group: inference.networking.k8s.io
kind: InferencePool
port: 80
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: multi-route-infpool-2
namespace: default
spec:
parentRefs:
- name: gateway
namespace: higress-system
hostnames: ["multi-infpool.domain.example"]
rules:
- matches:
- path:
type: PathPrefix
value: /model2
backendRefs:
- name: infpool-model2
group: inference.networking.k8s.io
kind: InferencePool
port: 80
---
apiVersion: inference.networking.k8s.io/v1
kind: InferencePool
metadata:
name: infpool-model1
namespace: default
spec:
targetPorts:
- number: 8000
selector:
matchLabels:
app: model1-server
endpointPickerRef:
name: model1-epp
port:
number: 9002
---
apiVersion: inference.networking.k8s.io/v1
kind: InferencePool
metadata:
name: infpool-model2
namespace: default
spec:
targetPorts:
- number: 8000
selector:
matchLabels:
app: model2-server
endpointPickerRef:
name: model2-epp
port:
number: 9002

View File

@@ -23,13 +23,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/mirror.default,HTTPRoute/redirect.default,HTTPRoute/rewrite.default
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- match:
- uri:
@@ -242,6 +242,35 @@ spec:
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
annotations:
internal.istio.io/parents: HTTPRoute/multi-route-infpool-1.default,HTTPRoute/multi-route-infpool-2.default
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~multi-infpool.domain.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- multi-infpool.domain.example
http:
- match:
- uri:
prefix: /model1
name: default/multi-route-infpool-1
route:
- destination:
host: infpool-model1-ip-aaaaf2d6.default.svc.domain.suffix
- match:
- uri:
prefix: /model2
name: default/multi-route-infpool-2
route:
- destination:
host: infpool-model2-ip-f857bff9.default.svc.domain.suffix
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
annotations:
internal.istio.io/parents: HTTPRoute/redirect-prefix-replace.default

View File

@@ -142,13 +142,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/invalid-mirror.default
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- name: default/invalid-mirror
route:

View File

@@ -305,13 +305,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- name: default/bind-all
route:
@@ -326,13 +326,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/same-namespace-valid.istio-system
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-foobar~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-foobar~*.foobar.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-foobar
hosts:
- '*'
- '*.foobar.example'
http:
- name: default/bind-all
route:
@@ -374,13 +374,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-no~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-no~*.slctr-expr-dne-no.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-no
hosts:
- '*'
- '*.slctr-expr-dne-no.example'
http:
- name: default/bind-all
route:
@@ -401,13 +401,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-yes~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-yes~*.slctr-expr-dne-yes.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-yes
hosts:
- '*'
- '*.slctr-expr-dne-yes.example'
http:
- name: default/bind-all
route:
@@ -428,13 +428,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-no~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-no~*.slctr-expr-notin-no.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-no
hosts:
- '*'
- '*.slctr-expr-notin-no.example'
http:
- name: default/bind-all
route:
@@ -455,13 +455,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-yes~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-yes~*.slctr-expr-notin-yes.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-yes
hosts:
- '*'
- '*.slctr-expr-notin-yes.example'
http:
- name: default/bind-all
route:

View File

@@ -409,7 +409,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Protocol: string(protocol.HTTP),
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId.String()),
},
Hosts: []string{rule.Host},
Hosts: []string{common.WildcardHost(rule.Host)},
})
// Add new gateway, builder
@@ -422,6 +422,45 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
}
}
passthroughOwner := common.PassthroughTLSHostOwner(convertOptions, rule.Host)
standaloneSSLPassthrough := convertOptions.PassthroughTLSHostOwners == nil && wrapper.AnnotationsConfig.IsSSLPassthrough()
if common.SameConfig(passthroughOwner, cfg) || standaloneSSLPassthrough {
if rule.HTTP == nil || len(rule.HTTP.Paths) == 0 {
continue
}
if _, ok := rootHTTPIngressPath(rule.HTTP.Paths); !ok {
continue
}
domainBuilder.Protocol = common.HTTPS
if wrapperGateway.IsHTTPS() {
if common.SameConfig(preDomainBuilder.Ingress, cfg) {
continue
}
domainBuilder.Event = common.DuplicatedTls
domainBuilder.PreIngress = preDomainBuilder.Ingress
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
domainBuilder.Build())
continue
}
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers,
common.CreateSSLPassthroughServer(rule.Host, c.options.GatewayHttpsPort, c.options.ClusterId))
convertOptions.IngressDomainCache.Valid[rule.Host] = domainBuilder
continue
}
if wrapper.AnnotationsConfig.IsSSLPassthrough() {
if rule.HTTP != nil {
if _, ok := rootHTTPIngressPath(rule.HTTP.Paths); ok && passthroughOwner != nil {
domainBuilder.Protocol = common.HTTPS
domainBuilder.Event = common.DuplicatedTls
domainBuilder.PreIngress = passthroughOwner
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
domainBuilder.Build())
}
}
continue
}
// There are no tls settings, so just skip.
if len(ingressV1Beta.TLS) == 0 {
continue
@@ -470,6 +509,14 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
domainBuilder.SecretName = path.Join(c.options.ClusterId.String(), cfg.Namespace, secretName)
if passthroughOwner != nil {
domainBuilder.Event = common.DuplicatedTls
domainBuilder.PreIngress = passthroughOwner
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
domainBuilder.Build())
continue
}
// There is a matching secret and the gateway has already a tls secret.
// We should report the duplicated tls secret event.
if wrapperGateway.IsHTTPS() {
@@ -487,7 +534,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Protocol: string(protocol.HTTPS),
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId.String()),
},
Hosts: []string{rule.Host},
Hosts: []string{common.WildcardHost(rule.Host)},
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CredentialName: credentials.ToKubernetesIngressResource(c.options.RawClusterId, secretNamespace, secretName),
@@ -515,6 +562,19 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
return nil
}
if convertOptions.Route2Ingress == nil {
convertOptions.Route2Ingress = map[string]*common.WrapperConfigWithRuleKey{}
}
if convertOptions.IngressRouteCache == nil {
convertOptions.IngressRouteCache = common.NewIngressRouteCache()
}
if convertOptions.VirtualServices == nil {
convertOptions.VirtualServices = map[string]*common.WrapperVirtualService{}
}
if convertOptions.HTTPRoutes == nil {
convertOptions.HTTPRoutes = map[string][]*common.WrapperHTTPRoute{}
}
cfg := wrapper.Config
ingressV1, ok := cfg.Spec.(ingress.IngressSpec)
if !ok {
@@ -546,12 +606,7 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
wrapperVS, exist := convertOptions.VirtualServices[rule.Host]
if !exist {
wrapperVS = &common.WrapperVirtualService{
VirtualService: &networking.VirtualService{
Hosts: []string{rule.Host},
},
WrapperConfig: wrapper,
}
wrapperVS = common.NewWrapperVirtualService(rule.Host, wrapper)
convertOptions.VirtualServices[rule.Host] = wrapperVS
}
@@ -579,7 +634,11 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
ingressPathType := defaultPathType
if httpPath.PathType != nil {
ingressPathType = *httpPath.PathType
}
switch ingressPathType {
case ingress.PathTypeExact:
pathType = common.Exact
case ingress.PathTypePrefix:
@@ -661,9 +720,84 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
common.SortHTTPRoutes(routes)
}
if common.HasPassthroughTLSHostOwner(convertOptions, cfg) ||
(convertOptions.PassthroughTLSHostOwners == nil && wrapper.AnnotationsConfig.IsSSLPassthrough()) {
return c.ConvertTLSRoute(convertOptions, wrapper)
}
return nil
}
func (c *controller) ConvertTLSRoute(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
if convertOptions == nil {
return fmt.Errorf("convertOptions is nil")
}
if wrapper == nil {
return fmt.Errorf("wrapperConfig is nil")
}
if convertOptions.VirtualServices == nil {
convertOptions.VirtualServices = map[string]*common.WrapperVirtualService{}
}
cfg := wrapper.Config
ingressV1Beta, ok := cfg.Spec.(ingress.IngressSpec)
if !ok {
common.IncrementInvalidIngress(c.options.ClusterId, common.Unknown)
return fmt.Errorf("convert type is invalid in cluster %s", c.options.ClusterId)
}
if len(ingressV1Beta.Rules) == 0 {
common.IncrementInvalidIngress(c.options.ClusterId, common.EmptyRule)
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
}
for _, rule := range ingressV1Beta.Rules {
if !common.IsPassthroughTLSHostOwner(convertOptions, cfg, rule.Host) {
IngressLog.Warnf("ignore duplicated ssl passthrough ingress rule %s:%s for host %q in cluster %s", cfg.Namespace, cfg.Name, rule.Host, c.options.ClusterId)
continue
}
if rule.HTTP == nil || len(rule.HTTP.Paths) == 0 {
IngressLog.Warnf("invalid ssl passthrough ingress rule %s:%s for host %q in cluster %s, no paths defined", cfg.Namespace, cfg.Name, rule.Host, c.options.ClusterId)
continue
}
httpPath, ok := rootHTTPIngressPath(rule.HTTP.Paths)
if !ok {
IngressLog.Warnf("ignore ssl passthrough ingress rule %s:%s for host %q in cluster %s, root path is not defined", cfg.Namespace, cfg.Name, rule.Host, c.options.ClusterId)
continue
}
wrapperVS, exist := convertOptions.VirtualServices[rule.Host]
if !exist {
wrapperVS = common.NewWrapperVirtualService(rule.Host, wrapper)
convertOptions.VirtualServices[rule.Host] = wrapperVS
} else if wrapperVS.HasTLSRouteForHost(rule.Host) {
continue
}
routeDestination, event := c.backendToTLSRouteDestination(&httpPath.Backend, cfg.Namespace, wrapper.AnnotationsConfig.Destination)
if event != common.Normal {
common.IncrementInvalidIngress(c.options.ClusterId, event)
continue
}
wrapperVS.VirtualService.Tls = append(wrapperVS.VirtualService.Tls,
common.CreateTLSRoute(rule.Host, routeDestination))
}
return nil
}
func rootHTTPIngressPath(paths []ingress.HTTPIngressPath) (*ingress.HTTPIngressPath, bool) {
for idx := range paths {
if paths[idx].Path == "" || paths[idx].Path == "/" {
return &paths[idx], true
}
}
return nil, false
}
func (c *controller) ApplyDefaultBackend(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
if convertOptions == nil {
return fmt.Errorf("convertOptions is nil")
@@ -691,12 +825,7 @@ func (c *controller) ApplyDefaultBackend(convertOptions *common.ConvertOptions,
wirecardVS, exist := convertOptions.VirtualServices[host]
if !exist || !wirecardVS.ConfiguredDefaultBackend {
if !exist {
wirecardVS = &common.WrapperVirtualService{
VirtualService: &networking.VirtualService{
Hosts: []string{host},
},
WrapperConfig: wrapper,
}
wirecardVS = common.NewWrapperVirtualService(host, wrapper)
}
specDefaultBackend := c.createDefaultRoute(wrapper, ingressV1Beta1.Backend, "*")
@@ -790,7 +919,11 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
ingressPathType := defaultPathType
if httpPath.PathType != nil {
ingressPathType = *httpPath.PathType
}
switch ingressPathType {
case ingress.PathTypeExact:
pathType = common.Exact
case ingress.PathTypePrefix:
@@ -1092,6 +1225,53 @@ func (c *controller) backendToRouteDestination(backend *ingress.IngressBackend,
}, common.Normal
}
func (c *controller) backendToTLSRouteDestination(backend *ingress.IngressBackend, namespace string,
config *annotations.DestinationConfig,
) ([]*networking.RouteDestination, common.Event) {
if backend == nil {
return nil, common.InvalidBackendService
}
if backend.ServiceName == "" {
if config != nil && len(config.McpDestination) > 0 {
return httpRouteDestinationToRouteDestination(config.McpDestination), common.Normal
}
return nil, common.InvalidBackendService
}
port := &networking.PortSelector{}
if backend.ServicePort.Type == intstr.Int {
port.Number = uint32(backend.ServicePort.IntVal)
} else {
resolvedPort, err := resolveNamedPort(backend, namespace, c.serviceLister)
if err != nil {
return nil, common.PortNameResolveError
}
port.Number = uint32(resolvedPort)
}
return []*networking.RouteDestination{
{
Destination: &networking.Destination{
Host: util.CreateServiceFQDN(namespace, backend.ServiceName),
Port: port,
},
Weight: 100,
},
}, common.Normal
}
func httpRouteDestinationToRouteDestination(destinations []*networking.HTTPRouteDestination) []*networking.RouteDestination {
out := make([]*networking.RouteDestination, 0, len(destinations))
for _, destination := range destinations {
out = append(out, &networking.RouteDestination{
Destination: destination.Destination,
Weight: destination.Weight,
})
}
return out
}
func resolveNamedPort(backend *ingress.IngressBackend, namespace string, serviceLister listerv1.ServiceLister) (int32, error) {
if backend == nil {
return 0, fmt.Errorf("ingressBackend is nil")

View File

@@ -16,9 +16,11 @@ package ingress
import (
"context"
"strings"
"testing"
"time"
"github.com/alibaba/higress/v2/pkg/cert"
"github.com/google/go-cmp/cmp"
"istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
@@ -68,6 +70,962 @@ func TestIngressControllerApplies(t *testing.T) {
}
}
func TestSSLPassthroughConvertGatewayAndTLSRoute(t *testing.T) {
c := controller{
options: common.Options{
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
},
}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "app",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
gatewayOptions := &common.ConvertOptions{
Gateways: map[string]*common.WrapperGateway{},
IngressDomainCache: common.NewIngressDomainCache(),
}
if err := c.ConvertGateway(gatewayOptions, wrapper, nil); err != nil {
t.Fatalf("ConvertGateway() error = %v", err)
}
gateway := gatewayOptions.Gateways["example.com"].Gateway
if len(gateway.Servers) != 2 {
t.Fatalf("server count mismatch, want 2, got %d", len(gateway.Servers))
}
tlsServer := gateway.Servers[1]
if tlsServer.Port.Protocol != "TLS" {
t.Fatalf("protocol mismatch, want TLS, got %s", tlsServer.Port.Protocol)
}
if tlsServer.Port.Number != 443 {
t.Fatalf("port mismatch, want 443, got %d", tlsServer.Port.Number)
}
if tlsServer.Tls.GetMode() != v1alpha3.ServerTLSSettings_PASSTHROUGH {
t.Fatalf("tls mode mismatch, want PASSTHROUGH, got %s", tlsServer.Tls.GetMode())
}
routeOptions := &common.ConvertOptions{}
if err := c.ConvertHTTPRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertHTTPRoute() error = %v", err)
}
httpRoutes := routeOptions.HTTPRoutes["example.com"]
if len(httpRoutes) != 1 {
t.Fatalf("http route count mismatch, want 1, got %d", len(httpRoutes))
}
if got := httpRoutes[0].HTTPRoute.Route[0].Destination.Host; got != "app.default.svc.cluster.local" {
t.Fatalf("http destination host mismatch, got %s", got)
}
routes := routeOptions.VirtualServices["example.com"].VirtualService.Tls
if len(routes) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(routes))
}
route := routes[0]
if got := route.Match[0].SniHosts[0]; got != "example.com" {
t.Fatalf("sni host mismatch, want example.com, got %s", got)
}
if got := route.Route[0].Destination.Host; got != "app.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, got %s", got)
}
if got := route.Route[0].Destination.Port.Number; got != 443 {
t.Fatalf("destination port mismatch, got %d", got)
}
}
func TestSSLPassthroughConvertTLSRouteRejectsNilInputs(t *testing.T) {
c := controller{}
wrapper := &common.WrapperConfig{
Config: &config.Config{},
AnnotationsConfig: &annotations.Ingress{},
}
if err := c.ConvertTLSRoute(nil, wrapper); err == nil {
t.Fatal("ConvertTLSRoute() with nil convertOptions returned nil error")
}
if err := c.ConvertTLSRoute(&common.ConvertOptions{}, nil); err == nil {
t.Fatal("ConvertTLSRoute() with nil wrapper returned nil error")
}
}
func TestSSLPassthroughUsesConfiguredHTTPSPort(t *testing.T) {
c := controller{
options: common.Options{
GatewayHttpPort: 80,
GatewayHttpsPort: 8443,
},
}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "app",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
gatewayOptions := &common.ConvertOptions{
Gateways: map[string]*common.WrapperGateway{},
IngressDomainCache: common.NewIngressDomainCache(),
}
if err := c.ConvertGateway(gatewayOptions, wrapper, nil); err != nil {
t.Fatalf("ConvertGateway() error = %v", err)
}
tlsServer := gatewayOptions.Gateways["example.com"].Gateway.Servers[1]
if tlsServer.Port.Number != 8443 {
t.Fatalf("port mismatch, want 8443, got %d", tlsServer.Port.Number)
}
}
func TestSSLPassthroughCanaryIngressKeepsCanaryHandling(t *testing.T) {
c := controller{}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-canary",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "app-canary",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
Canary: &annotations.CanaryConfig{Enabled: true},
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
routeOptions := &common.ConvertOptions{}
if err := c.ConvertHTTPRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertHTTPRoute() error = %v", err)
}
if len(routeOptions.CanaryIngresses) != 1 {
t.Fatalf("canary ingress count mismatch, want 1, got %d", len(routeOptions.CanaryIngresses))
}
if len(routeOptions.VirtualServices) != 0 {
t.Fatalf("unexpected virtual services: %+v", routeOptions.VirtualServices)
}
}
func TestSSLPassthroughSkipsDuplicatedTLSHost(t *testing.T) {
c := controller{
options: common.Options{
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
},
}
primary := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-primary",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "primary",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
duplicate := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-duplicate",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "duplicate",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
options := &common.ConvertOptions{
Gateways: map[string]*common.WrapperGateway{},
IngressDomainCache: common.NewIngressDomainCache(),
PassthroughTLSHostOwners: map[string]*config.Config{"example.com": primary.Config},
}
if err := c.ConvertGateway(options, primary, nil); err != nil {
t.Fatalf("ConvertGateway(primary) error = %v", err)
}
if err := c.ConvertGateway(options, duplicate, nil); err != nil {
t.Fatalf("ConvertGateway(duplicate) error = %v", err)
}
options.VirtualServices = map[string]*common.WrapperVirtualService{}
if err := c.ConvertTLSRoute(options, duplicate); err != nil {
t.Fatalf("ConvertTLSRoute() error = %v", err)
}
if len(options.VirtualServices) != 0 {
t.Fatalf("unexpected virtual services: %+v", options.VirtualServices)
}
}
func TestSSLPassthroughDuplicateTLSHostUsesExistingGatewayOwner(t *testing.T) {
c := controller{
options: common.Options{
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
},
}
primary := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-primary",
},
Spec: ingress.IngressSpec{
TLS: []ingress.IngressTLS{
{Hosts: []string{"example.com"}},
},
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "primary",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{},
}
duplicate := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-duplicate",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "duplicate",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
httpsCredentialConfig := &cert.Config{
CredentialConfig: []cert.CredentialEntry{
{
Domains: []string{"example.com"},
TLSSecret: "default/example-tls",
},
},
}
options := &common.ConvertOptions{
Gateways: map[string]*common.WrapperGateway{},
IngressDomainCache: common.NewIngressDomainCache(),
}
if err := c.ConvertGateway(options, primary, httpsCredentialConfig); err != nil {
t.Fatalf("ConvertGateway(primary) error = %v", err)
}
if err := c.ConvertGateway(options, duplicate, httpsCredentialConfig); err != nil {
t.Fatalf("ConvertGateway(duplicate) error = %v", err)
}
if len(options.IngressDomainCache.Invalid) != 1 {
t.Fatalf("invalid domain count mismatch, want 1, got %d", len(options.IngressDomainCache.Invalid))
}
invalid := options.IngressDomainCache.Invalid[0]
if !strings.Contains(invalid.Error, "tls-primary") {
t.Fatalf("invalid domain error does not reference existing gateway owner: %s", invalid.Error)
}
}
func TestBackendToTLSRouteDestinationRejectsEmptyMCPDestination(t *testing.T) {
c := controller{}
backend := &ingress.IngressBackend{}
config := &annotations.DestinationConfig{}
destinations, event := c.backendToTLSRouteDestination(backend, "default", config)
if event != common.InvalidBackendService {
t.Fatalf("event mismatch, want InvalidBackendService, got %s", event)
}
if len(destinations) != 0 {
t.Fatalf("destination count mismatch, want 0, got %d", len(destinations))
}
}
func TestSSLPassthroughUsesFirstRootOwnerWhenLaterIngressEnablesPassthrough(t *testing.T) {
c := controller{
options: common.Options{
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
},
}
root := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
ingressV1Beta1Rule("example.com", ingressV1Beta1Path("/", "root", 443)),
},
},
},
AnnotationsConfig: &annotations.Ingress{},
}
passthrough := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "passthrough",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
ingressV1Beta1Rule("example.com", ingressV1Beta1Path("/passthrough", "passthrough", 443)),
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
options := &common.ConvertOptions{
Gateways: map[string]*common.WrapperGateway{},
IngressDomainCache: common.NewIngressDomainCache(),
PassthroughTLSHostOwners: map[string]*config.Config{"example.com": root.Config},
}
if err := c.ConvertGateway(options, root, nil); err != nil {
t.Fatalf("ConvertGateway(root) error = %v", err)
}
if err := c.ConvertGateway(options, passthrough, nil); err != nil {
t.Fatalf("ConvertGateway(passthrough) error = %v", err)
}
gateway := options.Gateways["example.com"].Gateway
if len(gateway.Servers) != 2 {
t.Fatalf("server count mismatch, want 2, got %d", len(gateway.Servers))
}
if gateway.Servers[1].Tls.GetMode() != v1alpha3.ServerTLSSettings_PASSTHROUGH {
t.Fatalf("tls mode mismatch, want PASSTHROUGH, got %s", gateway.Servers[1].Tls.GetMode())
}
routeOptions := &common.ConvertOptions{
PassthroughTLSHostOwners: map[string]*config.Config{"example.com": root.Config},
}
if err := c.ConvertHTTPRoute(routeOptions, root); err != nil {
t.Fatalf("ConvertHTTPRoute(root) error = %v", err)
}
if err := c.ConvertHTTPRoute(routeOptions, passthrough); err != nil {
t.Fatalf("ConvertHTTPRoute(passthrough) error = %v", err)
}
routes := routeOptions.VirtualServices["example.com"].VirtualService.Tls
if len(routes) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(routes))
}
if got := routes[0].Route[0].Destination.Host; got != "root.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, want root.default.svc.cluster.local, got %s", got)
}
}
func TestSSLPassthroughNonRootIngressDoesNotBlockLaterRootIngress(t *testing.T) {
c := controller{
options: common.Options{
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
},
}
nonRoot := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-non-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
ingressV1Beta1Rule("example.com", ingressV1Beta1Path("/api", "api", 8443)),
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
root := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
ingressV1Beta1Rule("example.com", ingressV1Beta1Path("/", "root", 443)),
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
options := &common.ConvertOptions{
Gateways: map[string]*common.WrapperGateway{},
IngressDomainCache: common.NewIngressDomainCache(),
}
if err := c.ConvertGateway(options, nonRoot, nil); err != nil {
t.Fatalf("ConvertGateway(nonRoot) error = %v", err)
}
if len(options.Gateways["example.com"].Gateway.Servers) != 1 {
t.Fatalf("non-root ingress server count mismatch, want 1, got %d", len(options.Gateways["example.com"].Gateway.Servers))
}
if err := c.ConvertGateway(options, root, nil); err != nil {
t.Fatalf("ConvertGateway(root) error = %v", err)
}
if options.Gateways["example.com"].Gateway.Servers[1].Tls.GetMode() != v1alpha3.ServerTLSSettings_PASSTHROUGH {
t.Fatal("root ingress did not create a TLS passthrough server")
}
options.VirtualServices = map[string]*common.WrapperVirtualService{}
if err := c.ConvertTLSRoute(options, root); err != nil {
t.Fatalf("ConvertTLSRoute(root) error = %v", err)
}
routes := options.VirtualServices["example.com"].VirtualService.Tls
if len(routes) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(routes))
}
if got := routes[0].Route[0].Destination.Host; got != "root.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, got %s", got)
}
}
func TestSSLPassthroughPreservesRepeatedHostInSameIngress(t *testing.T) {
c := controller{
options: common.Options{
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
},
}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-repeated-host",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/health",
Backend: ingress.IngressBackend{
ServiceName: "health",
ServicePort: intstr.FromInt(8443),
},
},
},
},
},
},
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "root",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
options := &common.ConvertOptions{
Gateways: map[string]*common.WrapperGateway{},
IngressDomainCache: common.NewIngressDomainCache(),
}
if err := c.ConvertGateway(options, wrapper, nil); err != nil {
t.Fatalf("ConvertGateway() error = %v", err)
}
options.VirtualServices = map[string]*common.WrapperVirtualService{}
if err := c.ConvertTLSRoute(options, wrapper); err != nil {
t.Fatalf("ConvertTLSRoute() error = %v", err)
}
routes := options.VirtualServices["example.com"].VirtualService.Tls
if len(routes) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(routes))
}
if got := routes[0].Route[0].Destination.Host; got != "root.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, got %s", got)
}
}
func TestSSLPassthroughUsesFirstRootBackendForRepeatedHost(t *testing.T) {
c := controller{}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-repeated-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "first",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "second",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
routeOptions := &common.ConvertOptions{}
if err := c.ConvertHTTPRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertHTTPRoute() error = %v", err)
}
routes := routeOptions.VirtualServices["example.com"].VirtualService.Tls
if len(routes) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(routes))
}
if got := routes[0].Route[0].Destination.Host; got != "first.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, got %s", got)
}
}
func TestSSLPassthroughHandlesMultipleHosts(t *testing.T) {
c := controller{}
testcases := []struct {
name string
rules []ingress.IngressRule
wantHosts []string
wantRoutes map[string]string
}{
{
name: "root path first",
rules: []ingress.IngressRule{
ingressV1Beta1Rule("first.example.com", ingressV1Beta1Path("/", "first", 443)),
ingressV1Beta1Rule("middle.example.com", ingressV1Beta1Path("/health", "middle", 8443)),
ingressV1Beta1Rule("last.example.com", ingressV1Beta1Path("/health", "last", 8443)),
},
wantHosts: []string{"first.example.com"},
wantRoutes: map[string]string{
"first.example.com": "first.default.svc.cluster.local",
},
},
{
name: "root path middle",
rules: []ingress.IngressRule{
ingressV1Beta1Rule("first.example.com", ingressV1Beta1Path("/health", "first", 8443)),
ingressV1Beta1Rule("middle.example.com", ingressV1Beta1Path("/", "middle", 443)),
ingressV1Beta1Rule("last.example.com", ingressV1Beta1Path("/health", "last", 8443)),
},
wantHosts: []string{"middle.example.com"},
wantRoutes: map[string]string{
"middle.example.com": "middle.default.svc.cluster.local",
},
},
{
name: "root path last",
rules: []ingress.IngressRule{
ingressV1Beta1Rule("first.example.com", ingressV1Beta1Path("/health", "first", 8443)),
ingressV1Beta1Rule("middle.example.com", ingressV1Beta1Path("/health", "middle", 8443)),
ingressV1Beta1Rule("last.example.com", ingressV1Beta1Path("/", "last", 443)),
},
wantHosts: []string{"last.example.com"},
wantRoutes: map[string]string{
"last.example.com": "last.default.svc.cluster.local",
},
},
{
name: "multiple root hosts",
rules: []ingress.IngressRule{
ingressV1Beta1Rule("first.example.com", ingressV1Beta1Path("/", "first", 443)),
ingressV1Beta1Rule("middle.example.com", ingressV1Beta1Path("/", "middle", 443)),
ingressV1Beta1Rule("last.example.com", ingressV1Beta1Path("/", "last", 443)),
},
wantHosts: []string{"first.example.com", "middle.example.com", "last.example.com"},
wantRoutes: map[string]string{
"first.example.com": "first.default.svc.cluster.local",
"middle.example.com": "middle.default.svc.cluster.local",
"last.example.com": "last.default.svc.cluster.local",
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-multi-host",
},
Spec: ingress.IngressSpec{
Rules: tc.rules,
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
routeOptions := &common.ConvertOptions{}
if err := c.ConvertHTTPRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertHTTPRoute() error = %v", err)
}
for _, host := range tc.wantHosts {
routes := routeOptions.VirtualServices[host].VirtualService.Tls
if len(routes) != 1 {
t.Fatalf("tls route count mismatch for host %s, want 1, got %d", host, len(routes))
}
if got := routes[0].Route[0].Destination.Host; got != tc.wantRoutes[host] {
t.Fatalf("destination host mismatch for host %s, want %s, got %s", host, tc.wantRoutes[host], got)
}
}
})
}
}
func ingressV1Beta1Path(path, service string, port int32) ingress.HTTPIngressPath {
return ingress.HTTPIngressPath{
Path: path,
Backend: ingress.IngressBackend{
ServiceName: service,
ServicePort: intstr.FromInt(int(port)),
},
}
}
func ingressV1Beta1Rule(host string, paths ...ingress.HTTPIngressPath) ingress.IngressRule {
return ingress.IngressRule{
Host: host,
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: paths,
},
},
}
}
func TestSSLPassthroughUsesRootPathBackend(t *testing.T) {
c := controller{}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/api",
Backend: ingress.IngressBackend{
ServiceName: "api",
ServicePort: intstr.FromInt(8443),
},
},
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "root",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
routeOptions := &common.ConvertOptions{}
if err := c.ConvertHTTPRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertHTTPRoute() error = %v", err)
}
routes := routeOptions.VirtualServices["example.com"].VirtualService.Tls
if len(routes) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(routes))
}
if got := routes[0].Route[0].Destination.Host; got != "root.default.svc.cluster.local" {
t.Fatalf("destination host mismatch, got %s", got)
}
}
func TestSSLPassthroughWildcardHostKeepsVirtualServiceConsistent(t *testing.T) {
c := controller{}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-wildcard",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/",
Backend: ingress.IngressBackend{
ServiceName: "root",
ServicePort: intstr.FromInt(443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
routeOptions := &common.ConvertOptions{}
if err := c.ConvertHTTPRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertHTTPRoute() error = %v", err)
}
if err := c.ConvertTLSRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertTLSRoute() error = %v", err)
}
vs := routeOptions.VirtualServices[""].VirtualService
if got := vs.Hosts; len(got) != 1 || got[0] != "*" {
t.Fatalf("virtual service hosts mismatch, got %+v", got)
}
if len(vs.Tls) != 1 {
t.Fatalf("tls route count mismatch, want 1, got %d", len(vs.Tls))
}
if got := vs.Tls[0].Match[0].SniHosts; len(got) != 1 || got[0] != "*" {
t.Fatalf("sni hosts mismatch, got %+v", got)
}
}
func TestSSLPassthroughIgnoresNonRootPath(t *testing.T) {
c := controller{}
wrapper := &common.WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Namespace: "default",
Name: "tls-passthrough-non-root",
},
Spec: ingress.IngressSpec{
Rules: []ingress.IngressRule{
{
Host: "example.com",
IngressRuleValue: ingress.IngressRuleValue{
HTTP: &ingress.HTTPIngressRuleValue{
Paths: []ingress.HTTPIngressPath{
{
Path: "/api",
Backend: ingress.IngressBackend{
ServiceName: "api",
ServicePort: intstr.FromInt(8443),
},
},
},
},
},
},
},
},
},
AnnotationsConfig: &annotations.Ingress{
SSLPassthrough: &annotations.SSLPassthroughConfig{Enabled: true},
},
}
routeOptions := &common.ConvertOptions{}
if err := c.ConvertHTTPRoute(routeOptions, wrapper); err != nil {
t.Fatalf("ConvertHTTPRoute() error = %v", err)
}
if len(routeOptions.HTTPRoutes["example.com"]) != 1 {
t.Fatalf("http route count mismatch, want 1, got %d", len(routeOptions.HTTPRoutes["example.com"]))
}
if routes := routeOptions.VirtualServices["example.com"].VirtualService.Tls; len(routes) != 0 {
t.Fatalf("unexpected tls routes: %+v", routes)
}
}
func testApplyCanaryIngress(t *testing.T, c common.IngressController) {
testcases := []struct {
description string

View File

@@ -337,6 +337,13 @@ func extractTLSSecretName(host string, tls []ingress.IngressTLS) string {
}
func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig, httpsCredentialConfig *cert.Config) error {
if convertOptions == nil {
return fmt.Errorf("convertOptions is nil")
}
if wrapper == nil {
return fmt.Errorf("wrapperConfig is nil")
}
// Ignore canary config.
if wrapper.AnnotationsConfig.IsCanary() {
return nil
@@ -382,7 +389,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Protocol: string(protocol.HTTP),
Name: common.CreateConvertedName("http-"+strconv.FormatUint(uint64(c.options.GatewayHttpPort), 10)+"-ingress", string(c.options.ClusterId)),
},
Hosts: []string{rule.Host},
Hosts: []string{common.WildcardHost(rule.Host)},
})
// Add new gateway, builder
@@ -395,6 +402,45 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
}
}
passthroughOwner := common.PassthroughTLSHostOwner(convertOptions, rule.Host)
standaloneSSLPassthrough := convertOptions.PassthroughTLSHostOwners == nil && wrapper.AnnotationsConfig.IsSSLPassthrough()
if common.SameConfig(passthroughOwner, cfg) || standaloneSSLPassthrough {
if rule.HTTP == nil || len(rule.HTTP.Paths) == 0 {
continue
}
if _, ok := rootHTTPIngressPath(rule.HTTP.Paths); !ok {
continue
}
domainBuilder.Protocol = common.HTTPS
if wrapperGateway.IsHTTPS() {
if common.SameConfig(preDomainBuilder.Ingress, cfg) {
continue
}
domainBuilder.Event = common.DuplicatedTls
domainBuilder.PreIngress = preDomainBuilder.Ingress
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
domainBuilder.Build())
continue
}
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers,
common.CreateSSLPassthroughServer(rule.Host, c.options.GatewayHttpsPort, c.options.ClusterId))
convertOptions.IngressDomainCache.Valid[rule.Host] = domainBuilder
continue
}
if wrapper.AnnotationsConfig.IsSSLPassthrough() {
if rule.HTTP != nil {
if _, ok := rootHTTPIngressPath(rule.HTTP.Paths); ok && passthroughOwner != nil {
domainBuilder.Protocol = common.HTTPS
domainBuilder.Event = common.DuplicatedTls
domainBuilder.PreIngress = passthroughOwner
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
domainBuilder.Build())
}
}
continue
}
// There are no tls settings, so just skip.
if len(ingressV1.TLS) == 0 {
continue
@@ -443,6 +489,14 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
domainBuilder.Protocol = common.HTTPS
domainBuilder.SecretName = path.Join(c.options.ClusterId.String(), cfg.Namespace, secretName)
if passthroughOwner != nil {
domainBuilder.Event = common.DuplicatedTls
domainBuilder.PreIngress = passthroughOwner
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
domainBuilder.Build())
continue
}
// There is a matching secret and the gateway has already a tls secret.
// We should report the duplicated tls secret event.
if wrapperGateway.IsHTTPS() {
@@ -460,7 +514,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Protocol: string(protocol.HTTPS),
Name: common.CreateConvertedName("https-"+strconv.FormatUint(uint64(c.options.GatewayHttpsPort), 10)+"-ingress", string(c.options.ClusterId)),
},
Hosts: []string{rule.Host},
Hosts: []string{common.WildcardHost(rule.Host)},
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CredentialName: credentials.ToKubernetesIngressResource(c.options.RawClusterId, secretNamespace, secretName),
@@ -475,12 +529,32 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
}
func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
if convertOptions == nil {
return fmt.Errorf("convertOptions is nil")
}
if wrapper == nil {
return fmt.Errorf("wrapperConfig is nil")
}
// Canary ingress will be processed in the end.
if wrapper.AnnotationsConfig.IsCanary() {
convertOptions.CanaryIngresses = append(convertOptions.CanaryIngresses, wrapper)
return nil
}
if convertOptions.Route2Ingress == nil {
convertOptions.Route2Ingress = map[string]*common.WrapperConfigWithRuleKey{}
}
if convertOptions.IngressRouteCache == nil {
convertOptions.IngressRouteCache = common.NewIngressRouteCache()
}
if convertOptions.VirtualServices == nil {
convertOptions.VirtualServices = map[string]*common.WrapperVirtualService{}
}
if convertOptions.HTTPRoutes == nil {
convertOptions.HTTPRoutes = map[string][]*common.WrapperHTTPRoute{}
}
cfg := wrapper.Config
ingressV1, ok := cfg.Spec.(ingress.IngressSpec)
if !ok {
@@ -515,12 +589,7 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
wrapperVS, exist := convertOptions.VirtualServices[rule.Host]
if !exist {
wrapperVS = &common.WrapperVirtualService{
VirtualService: &networking.VirtualService{
Hosts: []string{rule.Host},
},
WrapperConfig: wrapper,
}
wrapperVS = common.NewWrapperVirtualService(rule.Host, wrapper)
convertOptions.VirtualServices[rule.Host] = wrapperVS
}
@@ -549,7 +618,11 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
ingressPathType := defaultPathType
if httpPath.PathType != nil {
ingressPathType = *httpPath.PathType
}
switch ingressPathType {
case ingress.PathTypeExact:
pathType = common.Exact
case ingress.PathTypePrefix:
@@ -626,9 +699,84 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
}
}
if common.HasPassthroughTLSHostOwner(convertOptions, cfg) ||
(convertOptions.PassthroughTLSHostOwners == nil && wrapper.AnnotationsConfig.IsSSLPassthrough()) {
return c.ConvertTLSRoute(convertOptions, wrapper)
}
return nil
}
func (c *controller) ConvertTLSRoute(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
if convertOptions == nil {
return fmt.Errorf("convertOptions is nil")
}
if wrapper == nil {
return fmt.Errorf("wrapperConfig is nil")
}
if convertOptions.VirtualServices == nil {
convertOptions.VirtualServices = map[string]*common.WrapperVirtualService{}
}
cfg := wrapper.Config
ingressV1, ok := cfg.Spec.(ingress.IngressSpec)
if !ok {
common.IncrementInvalidIngress(c.options.ClusterId, common.Unknown)
return fmt.Errorf("convert type is invalid in cluster %s", c.options.ClusterId)
}
if len(ingressV1.Rules) == 0 {
common.IncrementInvalidIngress(c.options.ClusterId, common.EmptyRule)
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
}
for _, rule := range ingressV1.Rules {
if !common.IsPassthroughTLSHostOwner(convertOptions, cfg, rule.Host) {
IngressLog.Warnf("ignore duplicated ssl passthrough ingress rule %s:%s for host %q in cluster %s", cfg.Namespace, cfg.Name, rule.Host, c.options.ClusterId)
continue
}
if rule.HTTP == nil || len(rule.HTTP.Paths) == 0 {
IngressLog.Warnf("invalid ssl passthrough ingress rule %s:%s for host %q in cluster %s, no paths defined", cfg.Namespace, cfg.Name, rule.Host, c.options.ClusterId)
continue
}
httpPath, ok := rootHTTPIngressPath(rule.HTTP.Paths)
if !ok {
IngressLog.Warnf("ignore ssl passthrough ingress rule %s:%s for host %q in cluster %s, root path is not defined", cfg.Namespace, cfg.Name, rule.Host, c.options.ClusterId)
continue
}
wrapperVS, exist := convertOptions.VirtualServices[rule.Host]
if !exist {
wrapperVS = common.NewWrapperVirtualService(rule.Host, wrapper)
convertOptions.VirtualServices[rule.Host] = wrapperVS
} else if wrapperVS.HasTLSRouteForHost(rule.Host) {
continue
}
routeDestination, event := c.backendToTLSRouteDestination(&httpPath.Backend, cfg.Namespace, wrapper.AnnotationsConfig.Destination)
if event != common.Normal {
common.IncrementInvalidIngress(c.options.ClusterId, event)
continue
}
wrapperVS.VirtualService.Tls = append(wrapperVS.VirtualService.Tls,
common.CreateTLSRoute(rule.Host, routeDestination))
}
return nil
}
func rootHTTPIngressPath(paths []ingress.HTTPIngressPath) (*ingress.HTTPIngressPath, bool) {
for idx := range paths {
if paths[idx].Path == "" || paths[idx].Path == "/" {
return &paths[idx], true
}
}
return nil, false
}
func (c *controller) generateHttpMatches(pathType common.PathType, path string, wrapperVS *common.WrapperVirtualService) []*networking.HTTPMatchRequest {
var httpMatches []*networking.HTTPMatchRequest
@@ -689,12 +837,7 @@ func (c *controller) ApplyDefaultBackend(convertOptions *common.ConvertOptions,
wirecardVS, exist := convertOptions.VirtualServices[host]
if !exist || !wirecardVS.ConfiguredDefaultBackend {
if !exist {
wirecardVS = &common.WrapperVirtualService{
VirtualService: &networking.VirtualService{
Hosts: []string{host},
},
WrapperConfig: wrapper,
}
wirecardVS = common.NewWrapperVirtualService(host, wrapper)
convertOptions.VirtualServices[host] = wirecardVS
}
@@ -782,7 +925,11 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
ingressPathType := defaultPathType
if httpPath.PathType != nil {
ingressPathType = *httpPath.PathType
}
switch ingressPathType {
case ingress.PathTypeExact:
pathType = common.Exact
case ingress.PathTypePrefix:
@@ -1074,6 +1221,54 @@ func (c *controller) backendToRouteDestination(backend *ingress.IngressBackend,
}, common.Normal
}
func (c *controller) backendToTLSRouteDestination(backend *ingress.IngressBackend, namespace string,
config *annotations.DestinationConfig,
) ([]*networking.RouteDestination, common.Event) {
if backend == nil {
return nil, common.InvalidBackendService
}
if backend.Service == nil {
if config != nil && len(config.McpDestination) > 0 {
return httpRouteDestinationToRouteDestination(config.McpDestination), common.Normal
}
return nil, common.InvalidBackendService
}
service := backend.Service
port := &networking.PortSelector{}
if service.Port.Number > 0 {
port.Number = uint32(service.Port.Number)
} else {
resolvedPort, err := resolveNamedPort(service, namespace, c.serviceLister)
if err != nil {
return nil, common.PortNameResolveError
}
port.Number = uint32(resolvedPort)
}
return []*networking.RouteDestination{
{
Destination: &networking.Destination{
Host: util.CreateServiceFQDN(namespace, service.Name),
Port: port,
},
Weight: 100,
},
}, common.Normal
}
func httpRouteDestinationToRouteDestination(destinations []*networking.HTTPRouteDestination) []*networking.RouteDestination {
out := make([]*networking.RouteDestination, 0, len(destinations))
for _, destination := range destinations {
out = append(out, &networking.RouteDestination{
Destination: destination.Destination,
Weight: destination.Weight,
})
}
return out
}
func resolveNamedPort(service *ingress.IngressServiceBackend, namespace string, serviceLister listerv1.ServiceLister) (int32, error) {
svc, err := serviceLister.Services(namespace).Get(service.Name)
if err != nil {

Some files were not shown because too many files have changed in this diff Show More