Compare commits
2 Commits
265026a907
...
c267f77eee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c267f77eee | ||
|
|
013defbd35 |
41
README.md
41
README.md
@ -20,7 +20,8 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
|
|||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
- **Gateway**:调用方连接平台,由平台选择上游代理并转发 HTTP 或 HTTPS
|
- **Gateway**:调用方连接平台,由平台选择上游代理并转发 HTTP 或 HTTPS
|
||||||
CONNECT。当前已有传输、调度和保护链组件,命令进程与控制面快照客户端待装配。
|
CONNECT。`proxy-gateway` 已装配本地监听、指标探针和控制面 Register/Watch/ACK/
|
||||||
|
Runtime 会话;带凭据 Proxy 分发仍待闭环。
|
||||||
- **Distribution**:调用方按条件提取真实代理;成功提取即独占消费,不支持归还、
|
- **Distribution**:调用方按条件提取真实代理;成功提取即独占消费,不支持归还、
|
||||||
续租或状态查询。
|
续租或状态查询。
|
||||||
- **Admin**:运维人员查询状态、启停 Upstream、切换 Routing,并触发严格配置
|
- **Admin**:运维人员查询状态、启停 Upstream、切换 Routing,并触发严格配置
|
||||||
@ -43,11 +44,12 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
|
|||||||
- **Gateway 组件**:HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、重试、超时、
|
- **Gateway 组件**:HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、重试、超时、
|
||||||
目的地址保护、本地快照存储、容量调度和 Worker 控制面 Register/Watch/ACK/
|
目的地址保护、本地快照存储、容量调度和 Worker 控制面 Register/Watch/ACK/
|
||||||
Runtime 会话组件已有实现与定向测试。`SessionSupervisor` 会为可恢复控制面中断执行
|
Runtime 会话组件已有实现与定向测试。`SessionSupervisor` 会为可恢复控制面中断执行
|
||||||
有界退避重连。Controller 可向 Worker 下发无凭据引用的已归属 Proxy 及 Gateway
|
有界退避重连。Controller 可向 Worker 下发已归属 Proxy、Gateway Routing 与按引用去重的
|
||||||
Routing 快照。Gateway 会将 Routing 与 Proxy 原子编译为同一内存 View,并只按当前未过期
|
凭据材料快照。Gateway 会将 Routing、Proxy 与凭据原子编译为同一内存 View,并只按当前未过期
|
||||||
View 匹配请求,并在内存中按 Sequential、Random、Round Robin、Weighted 或 Least
|
View 匹配请求,并在内存中按 Sequential、Random、Round Robin、Weighted 或 Least
|
||||||
Connections 选择上游。无候选时支持 reject、受 `waitTimeout` 限制的本地容量等待,
|
Connections 选择上游。无候选时支持 reject、受 `waitTimeout` 限制的本地容量等待,
|
||||||
以及仍经过目标地址策略的 direct;`proxy-gateway` 命令与凭据分发仍待装配。
|
以及仍经过目标地址策略的 direct;`proxy-gateway` 通过独立控制面拨号地址维护
|
||||||
|
Session,并仅在持有未过期 Snapshot 时 Ready;凭据材料只保留在当前节点内存 View。
|
||||||
- **安全边界**:Gateway、Distribution 与 Admin 使用各自的认证语义,并支持
|
- **安全边界**:Gateway、Distribution 与 Admin 使用各自的认证语义,并支持
|
||||||
CIDR、可信代理、严格请求解析和敏感信息最小化。
|
CIDR、可信代理、严格请求解析和敏感信息最小化。
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
Client[调用方] -->|HTTP / CONNECT| Gateway[Gateway<br/>进程待装配]
|
Client[调用方] -->|HTTP / CONNECT| Gateway[proxy-gateway]
|
||||||
Client -->|独占提取| Distribution[Distribution]
|
Client -->|独占提取| Distribution[Distribution]
|
||||||
Operator[运维人员] -->|管理操作| Admin[Admin]
|
Operator[运维人员] -->|管理操作| Admin[Admin]
|
||||||
subgraph CP[proxy-controller 已运行]
|
subgraph CP[proxy-controller 已运行]
|
||||||
@ -66,7 +68,7 @@ flowchart LR
|
|||||||
Controller --> Redis[(Redis)]
|
Controller --> Redis[(Redis)]
|
||||||
Controller --> PostgreSQL[(PostgreSQL)]
|
Controller --> PostgreSQL[(PostgreSQL)]
|
||||||
Checker[Checker<br/>健康链待闭环] -. Observation .-> Controller
|
Checker[Checker<br/>健康链待闭环] -. Observation .-> Controller
|
||||||
Controller -. Snapshot 链待闭环 .-> Gateway
|
Controller -. gRPC Snapshot .-> Gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
- **PostgreSQL** 只保存管理面状态,不保存 Proxy 明细或逐次提取记录。
|
- **PostgreSQL** 只保存管理面状态,不保存 Proxy 明细或逐次提取记录。
|
||||||
@ -76,7 +78,7 @@ flowchart LR
|
|||||||
|
|
||||||
## 当前完成度
|
## 当前完成度
|
||||||
|
|
||||||
截至 **2026-07-31**,实施计划检查项为 **53 / 74(71.6%)**。详情见
|
截至 **2026-07-31**,实施计划检查项为 **55 / 74(74.3%)**。详情见
|
||||||
[实施计划](docs/development/implementation-plan.md)和
|
[实施计划](docs/development/implementation-plan.md)和
|
||||||
[交付完成度审计](docs/requirements/completion-audit.md)。
|
[交付完成度审计](docs/requirements/completion-audit.md)。
|
||||||
|
|
||||||
@ -84,9 +86,9 @@ flowchart LR
|
|||||||
提取与限流、Controller 的 Admin/Distribution/Metrics 监听,以及 PostgreSQL
|
提取与限流、Controller 的 Admin/Distribution/Metrics 监听,以及 PostgreSQL
|
||||||
管理状态;WorkerControlPlane 的 Register、Snapshot ACK、Runtime 心跳接收和
|
管理状态;WorkerControlPlane 的 Register、Snapshot ACK、Runtime 心跳接收和
|
||||||
Redis 会话栅栏。
|
Redis 会话栅栏。
|
||||||
- **部分完成**:Gateway 传输与调度组件、Snapshot 本地存储、Worker ownership
|
- **部分完成**:Gateway Outcome 上报、Checker 调度与健康状态链、Docker Compose/
|
||||||
与运行态领域组件、Docker Compose/Kubernetes 静态部署清单和 protobuf 契约。
|
Kubernetes 运行时 mTLS Overlay,以及 protobuf 契约。
|
||||||
- **待完成**:带凭据 Proxy 分发、Gateway 进程装配、Outcome 上报、Checker 调度与健康状态链,
|
- **待完成**:Outcome 上报、Checker 调度与健康状态链,
|
||||||
以及 loadgen 和代表性集群压测。
|
以及 loadgen 和代表性集群压测。
|
||||||
|
|
||||||
检查项数量不等于生产就绪度。静态部署清单与 protobuf descriptor 验证也不代表
|
检查项数量不等于生产就绪度。静态部署清单与 protobuf descriptor 验证也不代表
|
||||||
@ -137,7 +139,20 @@ go run ./cmd/proxy-controller -config CONFIG_FILE
|
|||||||
改用宿主机可达的存储地址。
|
改用宿主机可达的存储地址。
|
||||||
|
|
||||||
本地配置中的 `.invalid` Provider URL 是故障演示占位,不会提供真实代理。
|
本地配置中的 `.invalid` Provider URL 是故障演示占位,不会提供真实代理。
|
||||||
当前仓库没有 Gateway、Checker 或 loadgen 命令,因此不提供对应启动命令。
|
Gateway 已提供启动命令;需要先启用 Controller `controlPlane` 并配置匹配的 mTLS
|
||||||
|
证书(回环 fixture 可使用明文),再提供独立的拨号地址和 Worker 身份:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run ./cmd/proxy-gateway -config CONFIG_FILE `
|
||||||
|
-control-plane CONTROLLER_HOST:8443 `
|
||||||
|
-cluster-id CLUSTER_ID -worker-id WORKER_ID `
|
||||||
|
-instance-id INSTANCE_ID -zone ZONE
|
||||||
|
```
|
||||||
|
|
||||||
|
以上参数也可通过 `PROXY_POOL_CONTROL_PLANE_ADDRESS`、`PROXY_POOL_CLUSTER_ID`、
|
||||||
|
`PROXY_POOL_WORKER_ID`、`PROXY_POOL_INSTANCE_ID` 与 `PROXY_POOL_ZONE` 提供。
|
||||||
|
Gateway 的 `/livez`、`/readyz`、`/metrics` 使用配置中的 `metrics.listen`;无有效
|
||||||
|
Snapshot 时 `/readyz` 返回 `503`。Checker 与 loadgen 命令尚未实现。
|
||||||
|
|
||||||
## 关键配置与入口
|
## 关键配置与入口
|
||||||
|
|
||||||
@ -170,8 +185,8 @@ go run ./cmd/proxy-controller -config CONFIG_FILE
|
|||||||
ownership 索引,以及 Gateway 快照客户端。
|
ownership 索引,以及 Gateway 快照客户端。
|
||||||
- **P0 - Checker 健康链**:Checker 调度、实际探测、Observation reducer,以及
|
- **P0 - Checker 健康链**:Checker 调度、实际探测、Observation reducer,以及
|
||||||
`FETCHED -> AVAILABLE / SUSPECT / UNHEALTHY` 状态链。
|
`FETCHED -> AVAILABLE / SUSPECT / UNHEALTHY` 状态链。
|
||||||
- **P1 - Gateway 与 Routing**:`proxy-gateway` 命令、五种 Routing 策略、
|
- **P1 - Gateway 与 Routing**:Gateway 进程、快照凭据分发、五种 Routing 策略与
|
||||||
`onUnavailable`、动态容量调整和 Drain 闭环。
|
`onUnavailable` 已接入;动态容量调整和 Drain 闭环待完成。
|
||||||
- **P1 - 可观测与部署**:低基数业务指标、完整 Compose/Kubernetes 进程拓扑,
|
- **P1 - 可观测与部署**:低基数业务指标、完整 Compose/Kubernetes 进程拓扑,
|
||||||
以及故障转移和恢复演练。
|
以及故障转移和恢复演练。
|
||||||
- **P2 - 容量证明**:`proxy-loadgen`、HTTP/CONNECT/Extract 分场景压测,以及
|
- **P2 - 容量证明**:`proxy-loadgen`、HTTP/CONNECT/Extract 分场景压测,以及
|
||||||
|
|||||||
@ -64,6 +64,16 @@ message WorkerSnapshot {
|
|||||||
bytes checksum = 5;
|
bytes checksum = 5;
|
||||||
repeated RoutingRule routing = 6;
|
repeated RoutingRule routing = 6;
|
||||||
repeated OwnedProxy proxies = 7;
|
repeated OwnedProxy proxies = 7;
|
||||||
|
// Credential materials are protected by the WorkerControlPlane mTLS session
|
||||||
|
// and retained only in the Gateway's current in-memory snapshot view.
|
||||||
|
repeated SnapshotCredential credentials = 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SnapshotCredential {
|
||||||
|
string secret_ref = 1;
|
||||||
|
string credential_version = 2;
|
||||||
|
string username = 3;
|
||||||
|
string password = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SnapshotDelta {
|
message SnapshotDelta {
|
||||||
|
|||||||
97
cmd/proxy-gateway/main.go
Normal file
97
cmd/proxy-gateway/main.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"proxy-pool/internal/config"
|
||||||
|
"proxy-pool/internal/gateway/bootstrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configEnvironment = "PROXY_POOL_CONFIG"
|
||||||
|
controlPlaneAddressEnvironment = "PROXY_POOL_CONTROL_PLANE_ADDRESS"
|
||||||
|
clusterIDEnvironment = "PROXY_POOL_CLUSTER_ID"
|
||||||
|
workerIDEnvironment = "PROXY_POOL_WORKER_ID"
|
||||||
|
instanceIDEnvironment = "PROXY_POOL_INSTANCE_ID"
|
||||||
|
zoneEnvironment = "PROXY_POOL_ZONE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type environmentLookup func(string) string
|
||||||
|
type gatewayRun func(context.Context, bootstrap.Options) error
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
os.Exit(execute(ctx, os.Args[1:], os.Getenv, bootstrap.Run, os.Stderr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(
|
||||||
|
ctx context.Context,
|
||||||
|
args []string,
|
||||||
|
getenv environmentLookup,
|
||||||
|
run gatewayRun,
|
||||||
|
stderr io.Writer,
|
||||||
|
) int {
|
||||||
|
flags := flag.NewFlagSet("proxy-gateway", flag.ContinueOnError)
|
||||||
|
flags.SetOutput(stderr)
|
||||||
|
configPath := flags.String("config", "", "configuration file path")
|
||||||
|
controlPlaneAddress := flags.String("control-plane", "", "remote Controller control-plane address")
|
||||||
|
clusterID := flags.String("cluster-id", "", "cluster identifier")
|
||||||
|
workerID := flags.String("worker-id", "", "unique Worker identifier")
|
||||||
|
instanceID := flags.String("instance-id", "", "unique process instance identifier")
|
||||||
|
zone := flags.String("zone", "", "availability zone identifier")
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
if errors.Is(err, flag.ErrHelp) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if flags.NArg() != 0 {
|
||||||
|
_, _ = fmt.Fprintln(stderr, "proxy-gateway: unexpected positional arguments")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if getenv != nil {
|
||||||
|
setIfEmpty(configPath, getenv(configEnvironment))
|
||||||
|
setIfEmpty(controlPlaneAddress, getenv(controlPlaneAddressEnvironment))
|
||||||
|
setIfEmpty(clusterID, getenv(clusterIDEnvironment))
|
||||||
|
setIfEmpty(workerID, getenv(workerIDEnvironment))
|
||||||
|
setIfEmpty(instanceID, getenv(instanceIDEnvironment))
|
||||||
|
setIfEmpty(zone, getenv(zoneEnvironment))
|
||||||
|
}
|
||||||
|
if ctx == nil || run == nil || !validValue(*configPath) || !validValue(*controlPlaneAddress) ||
|
||||||
|
!validValue(*clusterID) || !validValue(*workerID) || !validValue(*instanceID) || !validValue(*zone) {
|
||||||
|
_, _ = fmt.Fprintf(stderr,
|
||||||
|
"proxy-gateway: -config, -control-plane, -cluster-id, -worker-id, -instance-id and -zone are required; "+
|
||||||
|
"environment fallbacks: %s, %s, %s, %s, %s, %s\n",
|
||||||
|
configEnvironment, controlPlaneAddressEnvironment, clusterIDEnvironment, workerIDEnvironment, instanceIDEnvironment, zoneEnvironment,
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
err := run(ctx, bootstrap.Options{
|
||||||
|
ConfigPath: *configPath, Resolver: config.OSResolver{}, ControlPlaneAddress: *controlPlaneAddress,
|
||||||
|
ClusterID: *clusterID, WorkerID: *workerID, InstanceID: *instanceID, Zone: *zone,
|
||||||
|
})
|
||||||
|
if err == nil || (errors.Is(err, context.Canceled) && ctx.Err() != nil) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(stderr, "proxy-gateway: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func setIfEmpty(target *string, value string) {
|
||||||
|
if target != nil && *target == "" {
|
||||||
|
*target = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validValue(value string) bool {
|
||||||
|
return value != "" && strings.TrimSpace(value) == value
|
||||||
|
}
|
||||||
66
cmd/proxy-gateway/main_test.go
Normal file
66
cmd/proxy-gateway/main_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"proxy-pool/internal/gateway/bootstrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecutePassesExplicitGatewayIdentity(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var captured bootstrap.Options
|
||||||
|
status := execute(context.Background(), []string{
|
||||||
|
"-config", "gateway.yaml",
|
||||||
|
"-control-plane", "127.0.0.1:8443",
|
||||||
|
"-cluster-id", "cluster-a",
|
||||||
|
"-worker-id", "worker-a",
|
||||||
|
"-instance-id", "instance-a",
|
||||||
|
"-zone", "zone-a",
|
||||||
|
}, func(string) string { return "" }, func(_ context.Context, options bootstrap.Options) error {
|
||||||
|
captured = options
|
||||||
|
return nil
|
||||||
|
}, io.Discard)
|
||||||
|
if status != 0 {
|
||||||
|
t.Fatalf("execute() status = %d, want 0", status)
|
||||||
|
}
|
||||||
|
if captured.ConfigPath != "gateway.yaml" || captured.ControlPlaneAddress != "127.0.0.1:8443" ||
|
||||||
|
captured.ClusterID != "cluster-a" || captured.WorkerID != "worker-a" ||
|
||||||
|
captured.InstanceID != "instance-a" || captured.Zone != "zone-a" {
|
||||||
|
t.Fatalf("bootstrap options = %+v", captured)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteReadsGatewayOptionsFromEnvironment(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
values := map[string]string{
|
||||||
|
configEnvironment: "gateway.yaml",
|
||||||
|
controlPlaneAddressEnvironment: "127.0.0.1:8443",
|
||||||
|
clusterIDEnvironment: "cluster-a",
|
||||||
|
workerIDEnvironment: "worker-a",
|
||||||
|
instanceIDEnvironment: "instance-a",
|
||||||
|
zoneEnvironment: "zone-a",
|
||||||
|
}
|
||||||
|
var captured bootstrap.Options
|
||||||
|
status := execute(context.Background(), nil, func(name string) string { return values[name] }, func(_ context.Context, options bootstrap.Options) error {
|
||||||
|
captured = options
|
||||||
|
return nil
|
||||||
|
}, io.Discard)
|
||||||
|
if status != 0 || captured.WorkerID != "worker-a" || captured.ControlPlaneAddress != "127.0.0.1:8443" {
|
||||||
|
t.Fatalf("execute() = (%d, %+v)", status, captured)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteRejectsIncompleteGatewayOptions(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var output strings.Builder
|
||||||
|
status := execute(context.Background(), []string{"-config", "gateway.yaml"}, func(string) string { return "" }, nil, &output)
|
||||||
|
if status != 2 || !strings.Contains(output.String(), controlPlaneAddressEnvironment) {
|
||||||
|
t.Fatalf("execute() = (%d, %q), want usage error naming missing environment", status, output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -235,9 +235,9 @@ Provider Parser 继续通过 `credentials.Store` 生成 `SecretRef` 和
|
|||||||
在活动池状态提交之前。Distribution 所需凭据只保存在 Proxy 硬 TTL 和幂等 TTL
|
在活动池状态提交之前。Distribution 所需凭据只保存在 Proxy 硬 TTL 和幂等 TTL
|
||||||
约束内,Extract 脚本可原子保存完整重放响应。
|
约束内,Extract 脚本可原子保存完整重放响应。
|
||||||
|
|
||||||
Gateway Snapshot 继续只携带凭据引用;Gateway 通过控制面下发到节点内存的
|
Gateway Snapshot 携带凭据引用及按引用去重的材料;材料只经 mTLS 控制面进入
|
||||||
凭据材料解析引用,不在请求热路径查询 Redis。凭据分发与轮换属于独立后续
|
Gateway 当前内存 View,不在请求热路径查询 Redis,也不进入 PostgreSQL。凭据轮换的
|
||||||
实现,不改变本 ADR 的活动池边界。
|
主动推送仍属于后续实现,不改变本 ADR 的活动池边界。
|
||||||
|
|
||||||
### 库存真值
|
### 库存真值
|
||||||
|
|
||||||
|
|||||||
@ -27,14 +27,17 @@ Controller 已实现并验证 `RegisterWorker`、`AcknowledgeSnapshot` 和
|
|||||||
|
|
||||||
`WatchSnapshots` 已在 Register 后发送与当前 ownership epoch 对应的基础完整快照,
|
`WatchSnapshots` 已在 Register 后发送与当前 ownership epoch 对应的基础完整快照,
|
||||||
Gateway 校验后 ACK 并开始 Runtime 心跳。Controller 会从 Redis 的有界 Worker ownership
|
Gateway 校验后 ACK 并开始 Runtime 心跳。Controller 会从 Redis 的有界 Worker ownership
|
||||||
索引构建无凭据引用的已归属 Proxy 内容,并将租约到期收紧到 Proxy 的
|
索引构建已归属 Proxy 内容,并将租约到期收紧到 Proxy 的 `usable_until`。Proxy 引用的
|
||||||
`usable_until`;带凭据 Proxy 会 fail-closed,直到凭据材料分发完成。完整 Snapshot
|
凭据材料按 `secret_ref + credential_version` 去重,随完整 Snapshot 经 mTLS 下发,仅保留在
|
||||||
|
Gateway 当前内存 View。完整 Snapshot
|
||||||
已从配置原始顺序和 Admin 当前状态合成 Gateway Routing,并与 Proxy 一起纳入 checksum;
|
已从配置原始顺序和 Admin 当前状态合成 Gateway Routing,并与 Proxy 一起纳入 checksum;
|
||||||
Gateway 已将该 payload 编译并原子发布到与 Proxy 相同版本的本地 View,动态 Router 只匹配
|
Gateway 已将该 payload 编译并原子发布到与 Proxy 相同版本的本地 View,动态 Router 只匹配
|
||||||
当前未过期 View;派发器已按五种策略从该 View 选择上游,且在 Proxy 容量耗尽时只在该
|
当前未过期 View;派发器已按五种策略从该 View 选择上游,且在 Proxy 容量耗尽时只在该
|
||||||
View 的其余候选中回退。`wait_timeout` 随 `on_unavailable=WAIT` 下发并在 Gateway 作为有界
|
View 的其余候选中回退。`wait_timeout` 随 `on_unavailable=WAIT` 下发并在 Gateway 作为有界
|
||||||
本地容量等待使用;`DIRECT` 仍先经过 TargetPolicy 再建立 HTTP/CONNECT 直连。增量、Gateway
|
本地容量等待使用;`DIRECT` 仍先经过 TargetPolicy 再建立 HTTP/CONNECT 直连。`proxy-gateway`
|
||||||
进程装配、Outcome 与 Checker 闭环尚未实现。`ReportOutcomes` 仍明确返回 `Unimplemented`;
|
已装配 Register/Watch/ACK/Runtime 会话、HTTP 代理监听和 Snapshot 就绪探针;控制面中断时
|
||||||
|
保持进程运行并以有界退避重连,未取得有效 Snapshot 的 Worker 不会 Ready。增量、带凭据
|
||||||
|
Outcome 与 Checker 闭环尚未实现。`ReportOutcomes` 仍明确返回 `Unimplemented`;
|
||||||
`100,000 QPS` 仍是未验证的设计目标。
|
`100,000 QPS` 仍是未验证的设计目标。
|
||||||
|
|
||||||
`WatchSnapshots` 建立时校验当前 session;每次签发快照引用时也把 `session_id`
|
`WatchSnapshots` 建立时校验当前 session;每次签发快照引用时也把 `session_id`
|
||||||
@ -162,4 +165,21 @@ Routing 决定 AVAILABLE、SUSPECT 或 UNHEALTHY,并更新 Redis 活动池,
|
|||||||
|
|
||||||
集群环境使用 mTLS,证书身份绑定 Worker/Checker 类型和环境。服务端校验
|
集群环境使用 mTLS,证书身份绑定 Worker/Checker 类型和环境。服务端校验
|
||||||
消息中的逻辑 ID 与证书授权一致,设置单消息大小、流持续时间、并发 Stream
|
消息中的逻辑 ID 与证书授权一致,设置单消息大小、流持续时间、并发 Stream
|
||||||
和上报批次上限。`secret_ref` 是受控引用,不在 Proto 中传播真实密码。
|
和上报批次上限。`secret_ref` 是受控引用;完整 Snapshot 的 `credentials` 在 mTLS 会话中
|
||||||
|
携带引用对应材料,Controller 和 Gateway 仅在内存处理,禁止写入 Redis/PostgreSQL、日志或指标。
|
||||||
|
|
||||||
|
## 9. Gateway 启动参数
|
||||||
|
|
||||||
|
Gateway 不复用 `controlPlane.listen` 作为客户端地址。`listen` 是 Controller 的
|
||||||
|
服务端绑定地址;每个 Gateway 必须显式提供以下独立参数或同名环境变量:
|
||||||
|
|
||||||
|
- `-control-plane` / `PROXY_POOL_CONTROL_PLANE_ADDRESS`:Controller 的可拨号地址。
|
||||||
|
- `-cluster-id` / `PROXY_POOL_CLUSTER_ID`:快照所属集群。
|
||||||
|
- `-worker-id` / `PROXY_POOL_WORKER_ID`:唯一逻辑 Worker。
|
||||||
|
- `-instance-id` / `PROXY_POOL_INSTANCE_ID`:唯一进程实例。
|
||||||
|
- `-zone` / `PROXY_POOL_ZONE`:实例可用区。
|
||||||
|
|
||||||
|
当 `controlPlane.tls.mode=mtls` 时,Gateway 使用 `controlPlane.gatewayTLS` 中独立的
|
||||||
|
客户端证书、私钥和 Controller CA 发起 TLS 1.3 连接;证书必须符合 Controller 的
|
||||||
|
SPIFFE Worker 身份校验。
|
||||||
|
`disabled` 仅接受回环控制面地址,供本地 fixture 使用。
|
||||||
|
|||||||
@ -21,8 +21,9 @@ go run ./cmd/proxy-controller -config CONFIG_FILE
|
|||||||
所有 Controller 副本必须一致,且该密钥不得放入 YAML 或 PostgreSQL。该入口已装配
|
所有 Controller 副本必须一致,且该密钥不得放入 YAML 或 PostgreSQL。该入口已装配
|
||||||
PostgreSQL 管理面迁移、Redis 活动池、Distribution/Admin 独立监听与优雅停机;
|
PostgreSQL 管理面迁移、Redis 活动池、Distribution/Admin 独立监听与优雅停机;
|
||||||
Controller Metrics 独立监听、`/livez`、`/readyz` 和基础 Prometheus 运行时指标;
|
Controller Metrics 独立监听、`/livez`、`/readyz` 和基础 Prometheus 运行时指标;
|
||||||
Provider 自动补池、分布式配额、动态重载和 Admin 低基数统计已装配。完整 Gateway、
|
Provider 自动补池、分布式配额、动态重载和 Admin 低基数统计已装配。Gateway 进程、
|
||||||
Checker、Worker 控制面与代表性负载验证仍在后续实施范围。
|
Worker 控制面会话和 Snapshot 就绪探针已装配;Checker、带凭据 Proxy 分发与代表性
|
||||||
|
负载验证仍在后续实施范围。
|
||||||
|
|
||||||
所有时间值使用 Go duration,例如 `500ms`、`30s`、`5m`。示例中的
|
所有时间值使用 Go duration,例如 `500ms`、`30s`、`5m`。示例中的
|
||||||
`${TOKEN}`、`${PASSWORD}`、`${POSTGRES_URL}` 等由加载器从同名环境变量
|
`${TOKEN}`、`${PASSWORD}`、`${POSTGRES_URL}` 等由加载器从同名环境变量
|
||||||
@ -166,6 +167,10 @@ controlPlane:
|
|||||||
clientCAFile: /run/secrets/worker-ca.pem
|
clientCAFile: /run/secrets/worker-ca.pem
|
||||||
trustDomain: proxy.example
|
trustDomain: proxy.example
|
||||||
environment: production
|
environment: production
|
||||||
|
gatewayTLS:
|
||||||
|
certFile: /run/secrets/gateway-cert.pem
|
||||||
|
keyFile: /run/secrets/gateway-key.pem
|
||||||
|
serverCAFile: /run/secrets/controller-ca.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
- `protocolVersion` 当前固定为 `1`。
|
- `protocolVersion` 当前固定为 `1`。
|
||||||
@ -175,6 +180,15 @@ controlPlane:
|
|||||||
- `tls.mode` 只能为 `disabled` 或 `mtls`。`disabled` 只允许回环监听;`mtls` 必须
|
- `tls.mode` 只能为 `disabled` 或 `mtls`。`disabled` 只允许回环监听;`mtls` 必须
|
||||||
同时配置证书、私钥、客户端 CA、全小写 DNS `trustDomain` 和单 URI 路径段
|
同时配置证书、私钥、客户端 CA、全小写 DNS `trustDomain` 和单 URI 路径段
|
||||||
`environment`。
|
`environment`。
|
||||||
|
- `gatewayTLS` 是 Gateway 的客户端证书、私钥和 Controller CA,与 `tls` 的服务端
|
||||||
|
证书和 Worker CA 分离。三项可以同时省略(未运行 Gateway),配置任一项时必须完整提供。
|
||||||
|
|
||||||
|
Gateway 连接 Controller 时使用独立启动参数而非 `controlPlane.listen`。至少设置
|
||||||
|
`PROXY_POOL_CONTROL_PLANE_ADDRESS`、`PROXY_POOL_CLUSTER_ID`、
|
||||||
|
`PROXY_POOL_WORKER_ID`、`PROXY_POOL_INSTANCE_ID` 和 `PROXY_POOL_ZONE`,详见
|
||||||
|
[控制面协议](../api/control-plane.md#9-gateway-启动参数)。基础 Compose/Kubernetes
|
||||||
|
模板保持 `controlPlane.enabled: false`,环境 Overlay 挂载 mTLS 证书并启用后才可启动
|
||||||
|
Gateway。
|
||||||
|
|
||||||
## 5. Gateway
|
## 5. Gateway
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ proxy-pool/
|
|||||||
├── internal/
|
├── internal/
|
||||||
│ ├── config/ # 严格配置解析和校验
|
│ ├── config/ # 严格配置解析和校验
|
||||||
│ ├── domain/ # 无传输、无存储依赖的领域模型
|
│ ├── domain/ # 无传输、无存储依赖的领域模型
|
||||||
│ ├── gateway/ # snapshot、dispatch、server、transport
|
│ ├── gateway/ # bootstrap、snapshot、dispatch、server、transport
|
||||||
│ ├── controller/ # provider、pool、extraction、operations、runtime、bootstrap
|
│ ├── controller/ # provider、pool、extraction、operations、runtime、bootstrap
|
||||||
│ ├── adapters/ # PostgreSQL、Redis、Provider API、内存适配
|
│ ├── adapters/ # PostgreSQL、Redis、Provider API、内存适配
|
||||||
│ └── platform/ # HTTP、安全、日志、指标、停机和进程装配
|
│ └── platform/ # HTTP、安全、日志、指标、停机和进程装配
|
||||||
@ -34,6 +34,10 @@ proxy-pool/
|
|||||||
选择、session 与重试资格等热路径决策;`transport` 独占连接池、上游握手和
|
选择、session 与重试资格等热路径决策;`transport` 独占连接池、上游握手和
|
||||||
隧道生命周期。任何包都不得从热路径反向调用 Controller 存储。
|
隧道生命周期。任何包都不得从热路径反向调用 Controller 存储。
|
||||||
|
|
||||||
|
`gateway/bootstrap` 只在进程启动时加载配置、建立 gRPC 控制面 Session、组装 HTTP
|
||||||
|
代理与 Metrics 监听;请求热路径只读取本地 Snapshot。`/readyz` 要求当前 Snapshot
|
||||||
|
尚未到期,控制面断开期间以有界退避重连而不访问 Redis/PostgreSQL。
|
||||||
|
|
||||||
### proxy-controller
|
### proxy-controller
|
||||||
|
|
||||||
Controller 是首版模块化单体。Provider、Pool、Routing 和 Extraction 共享
|
Controller 是首版模块化单体。Provider、Pool、Routing 和 Extraction 共享
|
||||||
|
|||||||
@ -212,16 +212,18 @@ Prometheus 运行时指标,三监听器隔离已通过测试;业务指标仍
|
|||||||
WorkerControlPlane 现已接入 Controller 生命周期:Register、ACK 和 Runtime
|
WorkerControlPlane 现已接入 Controller 生命周期:Register、ACK 和 Runtime
|
||||||
报告均经 Redis 服务端 TTL 的 session/issued-snapshot/ACK 栅栏校验;mTLS SPIFFE
|
报告均经 Redis 服务端 TTL 的 session/issued-snapshot/ACK 栅栏校验;mTLS SPIFFE
|
||||||
身份、消息/流限制和有界停机已实现。`WatchSnapshots` 会发送当前 epoch 的基础完整
|
身份、消息/流限制和有界停机已实现。`WatchSnapshots` 会发送当前 epoch 的基础完整
|
||||||
Snapshot 并保持连接;Gateway 已具备 Register/Watch/ACK/Runtime 会话协调组件。
|
Snapshot 并保持连接;Gateway 已具备 Register/Watch/ACK/Runtime 会话协调组件与
|
||||||
按 Worker 的可下发 ownership 索引已进入 Redis 原子脚本,并可构建无凭据引用的
|
`proxy-gateway` 进程装配。
|
||||||
已归属 Proxy payload。Snapshot 签发与 session 匹配在同一 Redis Lua 操作中完成,
|
按 Worker 的可下发 ownership 索引已进入 Redis 原子脚本,并可构建已归属 Proxy、
|
||||||
|
去重凭据材料和 Routing 的完整 payload。Snapshot 签发与 session 匹配在同一 Redis Lua 操作中完成,
|
||||||
重注册会清除旧引用,避免迟到 Stream 覆盖新 session。Worker 服务端会在最近完整
|
重注册会清除旧引用,避免迟到 Stream 覆盖新 session。Worker 服务端会在最近完整
|
||||||
Snapshot 的 `valid_until` 到达时结束流;公用 `SessionSupervisor` 已为 Gateway 调用方
|
Snapshot 的 `valid_until` 到达时结束流;公用 `SessionSupervisor` 已为 Gateway 调用方
|
||||||
提供可恢复错误的有界指数退避重连,并在参数/认证/协议错误时停止。Gateway Routing
|
提供可恢复错误的有界指数退避重连,并在参数/认证/协议错误时停止。Gateway Routing
|
||||||
payload 已按配置顺序和 Admin revision/current 状态发布并覆盖 checksum;Gateway 已将其与
|
payload 已按配置顺序和 Admin revision/current 状态发布并覆盖 checksum;Gateway 已将其与
|
||||||
Proxy 原子编译为同版本 View,动态 Router 只匹配该未过期 View。派发器的五种上游选择已
|
Proxy 原子编译为同版本 View,动态 Router 只匹配该未过期 View。派发器的五种上游选择已
|
||||||
接入该 View,并在容量耗尽时在同版本候选中回退;`onUnavailable` 的 reject、wait 与 direct
|
接入该 View,并在容量耗尽时在同版本候选中回退;`onUnavailable` 的 reject、wait 与 direct
|
||||||
已接入 Gateway,凭据分发、Gateway 命令与 Outcome 上报仍未实现。
|
已接入 Gateway;`proxy-gateway` 已装配本地 HTTP/Metrics 监听、快照就绪探针和
|
||||||
|
控制面重连与快照凭据分发,Outcome 上报仍未实现。
|
||||||
|
|
||||||
已新增公用 `domain/activitypool` 契约及并发安全内存参考实现,Provider
|
已新增公用 `domain/activitypool` 契约及并发安全内存参考实现,Provider
|
||||||
Reconciler 通过 `UpsertFetched` 写入带供应商 TTL 和分配安全余量的批次;已覆盖
|
Reconciler 通过 `UpsertFetched` 写入带供应商 TTL 和分配安全余量的批次;已覆盖
|
||||||
@ -258,7 +260,7 @@ Controller 多副本共享同一计数。Client 身份只以 SHA-256 摘要进
|
|||||||
原子回收历史字段;Redis 异常 fail-closed 并返回 503,真实额度耗尽返回 429。
|
原子回收历史字段;Redis 异常 fail-closed 并返回 503,真实额度耗尽返回 429。
|
||||||
Gateway 请求热路径仍只使用本地准入,不增加 Redis/PostgreSQL 调用。
|
Gateway 请求热路径仍只使用本地准入,不增加 Redis/PostgreSQL 调用。
|
||||||
WorkerControlPlane gRPC 接收端、session 签发/心跳、Snapshot ACK 账本、基础
|
WorkerControlPlane gRPC 接收端、session 签发/心跳、Snapshot ACK 账本、基础
|
||||||
Snapshot 流和 Gateway 会话客户端已完成;权威 Proxy/Routing 发布、凭据分发、
|
Snapshot 流、Gateway 会话客户端与快照凭据分发已完成;权威 Proxy/Routing 发布、
|
||||||
Outcome 和健康执行链仍待完成,因此 Task 10 尚未全部完成。
|
Outcome 和健康执行链仍待完成,因此 Task 10 尚未全部完成。
|
||||||
|
|
||||||
## Task 11: Checker and Health Reducer
|
## Task 11: Checker and Health Reducer
|
||||||
|
|||||||
@ -37,7 +37,9 @@
|
|||||||
- `OPS-001`:完整 Snapshot 目标、epoch/version、校验和验证及原子替换。
|
- `OPS-001`:完整 Snapshot 目标、epoch/version、校验和验证及原子替换。
|
||||||
- `CAP-001 / GW 热路径边界`:本地 Dispatch 条件过滤与原子容量预留。
|
- `CAP-001 / GW 热路径边界`:本地 Dispatch 条件过滤与原子容量预留。
|
||||||
- `GW-*`:HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、超时、重试、保护链与
|
- `GW-*`:HTTP 正向代理、HTTPS CONNECT、双向 Tunnel、超时、重试、保护链与
|
||||||
优雅停机 Handler 已实现并通过定向测试。
|
优雅停机 Handler 已实现并通过定向测试。`proxy-gateway` 已装配本地 HTTP/Metrics
|
||||||
|
监听、快照就绪探针、显式控制面拨号和 Register/Watch/ACK/Runtime 会话;本地 gRPC
|
||||||
|
集成测试验证收到有效 Snapshot 后才 Ready,并可完成 direct 路由。
|
||||||
- `PROVIDER-*`:Provider HTTP Client、严格响应上限、模板解析安全边界、凭据
|
- `PROVIDER-*`:Provider HTTP Client、严格响应上限、模板解析安全边界、凭据
|
||||||
引用 Store 与 Reconciler Adapter 已实现。
|
引用 Store 与 Reconciler Adapter 已实现。
|
||||||
- `DIST/Admin HTTP`:严格 JSON、Request ID、Problem 响应及 Distribution/Admin
|
- `DIST/Admin HTTP`:严格 JSON、Request ID、Problem 响应及 Distribution/Admin
|
||||||
@ -51,9 +53,10 @@
|
|||||||
服务端 TTL,负向 ACK 会关闭 Runtime 写入栅栏。Controller 已装配 loopback
|
服务端 TTL,负向 ACK 会关闭 Runtime 写入栅栏。Controller 已装配 loopback
|
||||||
plaintext fixture 与 SPIFFE mTLS 服务端;基础 Snapshot 流和 Gateway 的
|
plaintext fixture 与 SPIFFE mTLS 服务端;基础 Snapshot 流和 Gateway 的
|
||||||
Register/Watch/ACK/Runtime 会话协调已实现。Redis 以 Worker 可下发 ownership
|
Register/Watch/ACK/Runtime 会话协调已实现。Redis 以 Worker 可下发 ownership
|
||||||
索引构建无凭据引用的已归属 Proxy payload,并以租约收紧可用期;Gateway Routing
|
索引构建已归属 Proxy payload,并以租约收紧可用期;凭据材料按引用去重并与 Proxy/
|
||||||
payload 已由配置顺序和 Admin 当前状态合成并覆盖 Snapshot checksum,其运行时消费、
|
Routing 一起纳入 Snapshot checksum,仅在 mTLS 和 Gateway 当前内存 View 中处理。Gateway
|
||||||
凭据分发、Outcome 和 Checker 尚未闭环。Snapshot 签发在 Redis 中原子匹配当前
|
Routing payload 已由配置顺序和 Admin 当前状态合成并覆盖 Snapshot checksum,其运行时消费、
|
||||||
|
Outcome 和 Checker 尚未闭环。Snapshot 签发在 Redis 中原子匹配当前
|
||||||
`session_id`,重注册会清除旧引用,迟到旧 Stream 不会覆盖新 session。Controller
|
`session_id`,重注册会清除旧引用,迟到旧 Stream 不会覆盖新 session。Controller
|
||||||
在最近成功下发的 Snapshot `valid_until` 到达时关闭流;Gateway 的公用
|
在最近成功下发的 Snapshot `valid_until` 到达时关闭流;Gateway 的公用
|
||||||
`SessionSupervisor` 已实现可恢复错误的有界退避重连。Gateway 会校验并执行 Snapshot
|
`SessionSupervisor` 已实现可恢复错误的有界退避重连。Gateway 会校验并执行 Snapshot
|
||||||
@ -88,10 +91,10 @@ CI 已配置 Linux race job。PostgreSQL 18 和 Redis 8.2 的隔离 Adapter fixt
|
|||||||
|
|
||||||
以下已有设计、接口或部署位置,但尚无端到端生产实现:
|
以下已有设计、接口或部署位置,但尚无端到端生产实现:
|
||||||
|
|
||||||
1. `cmd/proxy-gateway/checker/loadgen` 进程装配;`proxy-controller` 已完成
|
1. `cmd/proxy-checker/loadgen` 进程装配;`proxy-controller` 已完成
|
||||||
Admin/Distribution/Metrics 与 PostgreSQL/Redis 启动装配,但 Provider 和业务
|
Admin/Distribution/Metrics 与 PostgreSQL/Redis 启动装配,`proxy-gateway` 已完成
|
||||||
指标链未闭环。
|
HTTP/Metrics 与控制面 Session 装配,但 Provider 和业务指标链未闭环。
|
||||||
2. Gateway 进程装配、生产连接池调优与代表性流量压测。
|
2. Gateway 的带凭据 Proxy 分发、生产连接池调优与代表性流量压测。
|
||||||
3. Provider 分布式 singleflight/Leader、长期凭据回收和累计额度执行器。
|
3. Provider 分布式 singleflight/Leader、长期凭据回收和累计额度执行器。
|
||||||
4. Controller 的 PostgreSQL 连接池、迁移和 pgx Adapter 启动装配已完成;
|
4. Controller 的 PostgreSQL 连接池、迁移和 pgx Adapter 启动装配已完成;
|
||||||
公用 bootstrap 已通过 PostgreSQL 18 + Redis 8.2 双存储集成,Controller
|
公用 bootstrap 已通过 PostgreSQL 18 + Redis 8.2 双存储集成,Controller
|
||||||
@ -99,9 +102,9 @@ CI 已配置 Linux race job。PostgreSQL 18 和 Redis 8.2 的隔离 Adapter fixt
|
|||||||
5. Worker Register/ACK/Runtime gRPC 接收路径、Redis 服务端 TTL 会话生命周期、
|
5. Worker Register/ACK/Runtime gRPC 接收路径、Redis 服务端 TTL 会话生命周期、
|
||||||
SPIFFE mTLS 校验和 Controller 生命周期接线已完成;Redis Provider Leader、
|
SPIFFE mTLS 校验和 Controller 生命周期接线已完成;Redis Provider Leader、
|
||||||
分布式请求额度、Distribution Client 限制和 Provider 状态丢失重建已完成。
|
分布式请求额度、Distribution Client 限制和 Provider 状态丢失重建已完成。
|
||||||
6. Worker 基础网络快照流、无凭据引用 Proxy/Gateway Routing payload、Gateway Snapshot
|
6. Worker 基础网络快照流、Proxy/Gateway Routing/凭据 Snapshot payload、Gateway Snapshot
|
||||||
客户端、同版本 Routing 编译/动态匹配、五种策略上游选择与 reject/wait/direct 已完成;凭据
|
客户端和进程装配、同版本 Routing 编译/动态匹配、五种策略上游选择与 reject/wait/direct 已完成;
|
||||||
分发与 Outcome 上报仍待完成。Redis ownership drain/ACK/过期回收及按 Worker 的可下发索引已完成。
|
Outcome 上报仍待完成。Redis ownership drain/ACK/过期回收及按 Worker 的可下发索引已完成。
|
||||||
7. Checker 调度、探测器和健康 reducer。
|
7. Checker 调度、探测器和健康 reducer。
|
||||||
8. Admin/Distribution 细粒度授权和审计查询;Distribution 分布式限流已完成。
|
8. Admin/Distribution 细粒度授权和审计查询;Distribution 分布式限流已完成。
|
||||||
9. 真实 Compose/Kubernetes 集成、故障演练和代表性集群负载测试。
|
9. 真实 Compose/Kubernetes 集成、故障演练和代表性集群负载测试。
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
| ID | 最终需求 | 来源 | 验证证据 |
|
| ID | 最终需求 | 来源 | 验证证据 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ARCH-001 | 数据面 Worker 与控制面 Controller 分离 | 1-70 | 包、协议和部署拓扑已分离;Controller 已运行 Worker Register/Watch/ACK/Runtime gRPC,并发布无凭据 Proxy/Gateway Routing 完整快照;Gateway 已将同版本 Routing 编译为动态匹配 View。Gateway/Checker/Loadgen 构建产物、策略派发与 `onUnavailable` 待实现 |
|
| ARCH-001 | 数据面 Worker 与控制面 Controller 分离 | 1-70 | 包、协议和部署拓扑已分离;Controller 已运行 Worker Register/Watch/ACK/Runtime gRPC,并发布 Proxy/Gateway Routing/按引用去重凭据完整快照;Gateway 已将它们编译为同版本动态 View,并由独立进程维护控制面会话。Checker/Loadgen 构建产物待实现 |
|
||||||
| ARCH-002 | 热路径只做认证、本地路由和网络转发 | 1-70, 380-430 | Snapshot/Dispatch 及依赖边界已验证;完整 Gateway 进程与代表性性能剖析待完成 |
|
| ARCH-002 | 热路径只做认证、本地路由和网络转发 | 1-70, 380-430 | Gateway bootstrap 集成测试验证启动期控制面会话与快照就绪,HTTP 请求只走本地 Snapshot/Dispatch;代表性性能剖析待完成 |
|
||||||
| ARCH-003 | Gateway、Distribution、Admin、Metrics 独立入口 | 8904-8958 | Controller 命令已装配 Distribution/Admin/Metrics 三个独立监听及联动停机;Gateway 生产入口待装配 |
|
| ARCH-003 | Gateway、Distribution、Admin、Metrics 独立入口 | 8904-8958 | Controller 命令已装配 Distribution/Admin/Metrics 三个独立监听及联动停机;Gateway 命令已装配代理与 Metrics 监听,运行时 mTLS 部署 Overlay 待完成 |
|
||||||
| ARCH-004 | Controller 集中 Provider 获取与切换 | 1403-1580 | Redis Leader、动态 Provider Supervisor 与 Bootstrap 生产装配已完成;Admin disable/reload 驱动取消替换,多副本按权威 HMAC 指纹和 revision 栅栏收敛并拒绝旧配置换主;Routing 切换到 Drain 的编排待完成 |
|
| ARCH-004 | Controller 集中 Provider 获取与切换 | 1403-1580 | Redis Leader、动态 Provider Supervisor 与 Bootstrap 生产装配已完成;Admin disable/reload 驱动取消替换,多副本按权威 HMAC 指纹和 revision 栅栏收敛并拒绝旧配置换主;Routing 切换到 Drain 的编排待完成 |
|
||||||
| ARCH-005 | 100k QPS 峰值使用多 Worker 集群 | 当前会话 | 未验证设计目标;待代表性集群负载报告 |
|
| ARCH-005 | 100k QPS 峰值使用多 Worker 集群 | 当前会话 | 未验证设计目标;待代表性集群负载报告 |
|
||||||
|
|
||||||
|
|||||||
@ -604,6 +604,9 @@ type WorkerSnapshot struct {
|
|||||||
Checksum []byte `protobuf:"bytes,5,opt,name=checksum,proto3" json:"checksum,omitempty"`
|
Checksum []byte `protobuf:"bytes,5,opt,name=checksum,proto3" json:"checksum,omitempty"`
|
||||||
Routing []*RoutingRule `protobuf:"bytes,6,rep,name=routing,proto3" json:"routing,omitempty"`
|
Routing []*RoutingRule `protobuf:"bytes,6,rep,name=routing,proto3" json:"routing,omitempty"`
|
||||||
Proxies []*OwnedProxy `protobuf:"bytes,7,rep,name=proxies,proto3" json:"proxies,omitempty"`
|
Proxies []*OwnedProxy `protobuf:"bytes,7,rep,name=proxies,proto3" json:"proxies,omitempty"`
|
||||||
|
// Credential materials are protected by the WorkerControlPlane mTLS session
|
||||||
|
// and retained only in the Gateway's current in-memory snapshot view.
|
||||||
|
Credentials []*SnapshotCredential `protobuf:"bytes,14,rep,name=credentials,proto3" json:"credentials,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -687,6 +690,81 @@ func (x *WorkerSnapshot) GetProxies() []*OwnedProxy {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *WorkerSnapshot) GetCredentials() []*SnapshotCredential {
|
||||||
|
if x != nil {
|
||||||
|
return x.Credentials
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapshotCredential struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
SecretRef string `protobuf:"bytes,1,opt,name=secret_ref,json=secretRef,proto3" json:"secret_ref,omitempty"`
|
||||||
|
CredentialVersion string `protobuf:"bytes,2,opt,name=credential_version,json=credentialVersion,proto3" json:"credential_version,omitempty"`
|
||||||
|
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
|
||||||
|
Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SnapshotCredential) Reset() {
|
||||||
|
*x = SnapshotCredential{}
|
||||||
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SnapshotCredential) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SnapshotCredential) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SnapshotCredential) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SnapshotCredential.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SnapshotCredential) Descriptor() ([]byte, []int) {
|
||||||
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SnapshotCredential) GetSecretRef() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.SecretRef
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SnapshotCredential) GetCredentialVersion() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CredentialVersion
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SnapshotCredential) GetUsername() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Username
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SnapshotCredential) GetPassword() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Password
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type SnapshotDelta struct {
|
type SnapshotDelta struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
BaseVersion uint64 `protobuf:"varint,1,opt,name=base_version,json=baseVersion,proto3" json:"base_version,omitempty"`
|
BaseVersion uint64 `protobuf:"varint,1,opt,name=base_version,json=baseVersion,proto3" json:"base_version,omitempty"`
|
||||||
@ -704,7 +782,7 @@ type SnapshotDelta struct {
|
|||||||
|
|
||||||
func (x *SnapshotDelta) Reset() {
|
func (x *SnapshotDelta) Reset() {
|
||||||
*x = SnapshotDelta{}
|
*x = SnapshotDelta{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -716,7 +794,7 @@ func (x *SnapshotDelta) String() string {
|
|||||||
func (*SnapshotDelta) ProtoMessage() {}
|
func (*SnapshotDelta) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *SnapshotDelta) ProtoReflect() protoreflect.Message {
|
func (x *SnapshotDelta) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[5]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[6]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -729,7 +807,7 @@ func (x *SnapshotDelta) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use SnapshotDelta.ProtoReflect.Descriptor instead.
|
// Deprecated: Use SnapshotDelta.ProtoReflect.Descriptor instead.
|
||||||
func (*SnapshotDelta) Descriptor() ([]byte, []int) {
|
func (*SnapshotDelta) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{5}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SnapshotDelta) GetBaseVersion() uint64 {
|
func (x *SnapshotDelta) GetBaseVersion() uint64 {
|
||||||
@ -813,7 +891,7 @@ type RoutingRule struct {
|
|||||||
|
|
||||||
func (x *RoutingRule) Reset() {
|
func (x *RoutingRule) Reset() {
|
||||||
*x = RoutingRule{}
|
*x = RoutingRule{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[6]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[7]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -825,7 +903,7 @@ func (x *RoutingRule) String() string {
|
|||||||
func (*RoutingRule) ProtoMessage() {}
|
func (*RoutingRule) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoutingRule) ProtoReflect() protoreflect.Message {
|
func (x *RoutingRule) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[6]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[7]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -838,7 +916,7 @@ func (x *RoutingRule) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoutingRule.ProtoReflect.Descriptor instead.
|
||||||
func (*RoutingRule) Descriptor() ([]byte, []int) {
|
func (*RoutingRule) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{6}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{7}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoutingRule) GetName() string {
|
func (x *RoutingRule) GetName() string {
|
||||||
@ -922,7 +1000,7 @@ type RoutingStrategy struct {
|
|||||||
|
|
||||||
func (x *RoutingStrategy) Reset() {
|
func (x *RoutingStrategy) Reset() {
|
||||||
*x = RoutingStrategy{}
|
*x = RoutingStrategy{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[7]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[8]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -934,7 +1012,7 @@ func (x *RoutingStrategy) String() string {
|
|||||||
func (*RoutingStrategy) ProtoMessage() {}
|
func (*RoutingStrategy) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoutingStrategy) ProtoReflect() protoreflect.Message {
|
func (x *RoutingStrategy) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[7]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[8]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -947,7 +1025,7 @@ func (x *RoutingStrategy) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoutingStrategy.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoutingStrategy.ProtoReflect.Descriptor instead.
|
||||||
func (*RoutingStrategy) Descriptor() ([]byte, []int) {
|
func (*RoutingStrategy) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{7}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{8}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoutingStrategy) GetType() StrategyType {
|
func (x *RoutingStrategy) GetType() StrategyType {
|
||||||
@ -992,7 +1070,7 @@ type OwnedProxy struct {
|
|||||||
|
|
||||||
func (x *OwnedProxy) Reset() {
|
func (x *OwnedProxy) Reset() {
|
||||||
*x = OwnedProxy{}
|
*x = OwnedProxy{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[8]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1004,7 +1082,7 @@ func (x *OwnedProxy) String() string {
|
|||||||
func (*OwnedProxy) ProtoMessage() {}
|
func (*OwnedProxy) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *OwnedProxy) ProtoReflect() protoreflect.Message {
|
func (x *OwnedProxy) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[8]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[9]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1017,7 +1095,7 @@ func (x *OwnedProxy) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use OwnedProxy.ProtoReflect.Descriptor instead.
|
// Deprecated: Use OwnedProxy.ProtoReflect.Descriptor instead.
|
||||||
func (*OwnedProxy) Descriptor() ([]byte, []int) {
|
func (*OwnedProxy) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{8}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{9}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OwnedProxy) GetId() string {
|
func (x *OwnedProxy) GetId() string {
|
||||||
@ -1127,7 +1205,7 @@ type AcknowledgeSnapshotRequest struct {
|
|||||||
|
|
||||||
func (x *AcknowledgeSnapshotRequest) Reset() {
|
func (x *AcknowledgeSnapshotRequest) Reset() {
|
||||||
*x = AcknowledgeSnapshotRequest{}
|
*x = AcknowledgeSnapshotRequest{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[9]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[10]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1139,7 +1217,7 @@ func (x *AcknowledgeSnapshotRequest) String() string {
|
|||||||
func (*AcknowledgeSnapshotRequest) ProtoMessage() {}
|
func (*AcknowledgeSnapshotRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *AcknowledgeSnapshotRequest) ProtoReflect() protoreflect.Message {
|
func (x *AcknowledgeSnapshotRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[9]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[10]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1152,7 +1230,7 @@ func (x *AcknowledgeSnapshotRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use AcknowledgeSnapshotRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use AcknowledgeSnapshotRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*AcknowledgeSnapshotRequest) Descriptor() ([]byte, []int) {
|
func (*AcknowledgeSnapshotRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{9}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *AcknowledgeSnapshotRequest) GetWorkerId() string {
|
func (x *AcknowledgeSnapshotRequest) GetWorkerId() string {
|
||||||
@ -1223,7 +1301,7 @@ type OutcomeBatch struct {
|
|||||||
|
|
||||||
func (x *OutcomeBatch) Reset() {
|
func (x *OutcomeBatch) Reset() {
|
||||||
*x = OutcomeBatch{}
|
*x = OutcomeBatch{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[10]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[11]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1235,7 +1313,7 @@ func (x *OutcomeBatch) String() string {
|
|||||||
func (*OutcomeBatch) ProtoMessage() {}
|
func (*OutcomeBatch) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *OutcomeBatch) ProtoReflect() protoreflect.Message {
|
func (x *OutcomeBatch) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[10]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[11]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1248,7 +1326,7 @@ func (x *OutcomeBatch) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use OutcomeBatch.ProtoReflect.Descriptor instead.
|
// Deprecated: Use OutcomeBatch.ProtoReflect.Descriptor instead.
|
||||||
func (*OutcomeBatch) Descriptor() ([]byte, []int) {
|
func (*OutcomeBatch) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{10}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OutcomeBatch) GetWorkerId() string {
|
func (x *OutcomeBatch) GetWorkerId() string {
|
||||||
@ -1294,7 +1372,7 @@ type ProxyOutcome struct {
|
|||||||
|
|
||||||
func (x *ProxyOutcome) Reset() {
|
func (x *ProxyOutcome) Reset() {
|
||||||
*x = ProxyOutcome{}
|
*x = ProxyOutcome{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[11]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[12]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1306,7 +1384,7 @@ func (x *ProxyOutcome) String() string {
|
|||||||
func (*ProxyOutcome) ProtoMessage() {}
|
func (*ProxyOutcome) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ProxyOutcome) ProtoReflect() protoreflect.Message {
|
func (x *ProxyOutcome) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[11]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[12]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1319,7 +1397,7 @@ func (x *ProxyOutcome) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ProxyOutcome.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ProxyOutcome.ProtoReflect.Descriptor instead.
|
||||||
func (*ProxyOutcome) Descriptor() ([]byte, []int) {
|
func (*ProxyOutcome) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{11}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{12}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ProxyOutcome) GetProxyId() string {
|
func (x *ProxyOutcome) GetProxyId() string {
|
||||||
@ -1380,7 +1458,7 @@ type ReportOutcomesResponse struct {
|
|||||||
|
|
||||||
func (x *ReportOutcomesResponse) Reset() {
|
func (x *ReportOutcomesResponse) Reset() {
|
||||||
*x = ReportOutcomesResponse{}
|
*x = ReportOutcomesResponse{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[12]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[13]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1392,7 +1470,7 @@ func (x *ReportOutcomesResponse) String() string {
|
|||||||
func (*ReportOutcomesResponse) ProtoMessage() {}
|
func (*ReportOutcomesResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ReportOutcomesResponse) ProtoReflect() protoreflect.Message {
|
func (x *ReportOutcomesResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[12]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[13]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1405,7 +1483,7 @@ func (x *ReportOutcomesResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ReportOutcomesResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ReportOutcomesResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ReportOutcomesResponse) Descriptor() ([]byte, []int) {
|
func (*ReportOutcomesResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{12}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{13}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ReportOutcomesResponse) GetAcceptedThroughSequence() uint64 {
|
func (x *ReportOutcomesResponse) GetAcceptedThroughSequence() uint64 {
|
||||||
@ -1430,7 +1508,7 @@ type ReportRuntimeRequest struct {
|
|||||||
|
|
||||||
func (x *ReportRuntimeRequest) Reset() {
|
func (x *ReportRuntimeRequest) Reset() {
|
||||||
*x = ReportRuntimeRequest{}
|
*x = ReportRuntimeRequest{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[13]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[14]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1442,7 +1520,7 @@ func (x *ReportRuntimeRequest) String() string {
|
|||||||
func (*ReportRuntimeRequest) ProtoMessage() {}
|
func (*ReportRuntimeRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ReportRuntimeRequest) ProtoReflect() protoreflect.Message {
|
func (x *ReportRuntimeRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[13]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[14]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1455,7 +1533,7 @@ func (x *ReportRuntimeRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ReportRuntimeRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ReportRuntimeRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ReportRuntimeRequest) Descriptor() ([]byte, []int) {
|
func (*ReportRuntimeRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{13}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{14}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ReportRuntimeRequest) GetWorkerId() string {
|
func (x *ReportRuntimeRequest) GetWorkerId() string {
|
||||||
@ -1519,7 +1597,7 @@ type ProxyRuntime struct {
|
|||||||
|
|
||||||
func (x *ProxyRuntime) Reset() {
|
func (x *ProxyRuntime) Reset() {
|
||||||
*x = ProxyRuntime{}
|
*x = ProxyRuntime{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[14]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[15]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1531,7 +1609,7 @@ func (x *ProxyRuntime) String() string {
|
|||||||
func (*ProxyRuntime) ProtoMessage() {}
|
func (*ProxyRuntime) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ProxyRuntime) ProtoReflect() protoreflect.Message {
|
func (x *ProxyRuntime) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[14]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[15]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1544,7 +1622,7 @@ func (x *ProxyRuntime) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ProxyRuntime.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ProxyRuntime.ProtoReflect.Descriptor instead.
|
||||||
func (*ProxyRuntime) Descriptor() ([]byte, []int) {
|
func (*ProxyRuntime) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{14}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{15}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ProxyRuntime) GetProxyId() string {
|
func (x *ProxyRuntime) GetProxyId() string {
|
||||||
@ -1586,7 +1664,7 @@ type ReportRuntimeResponse struct {
|
|||||||
|
|
||||||
func (x *ReportRuntimeResponse) Reset() {
|
func (x *ReportRuntimeResponse) Reset() {
|
||||||
*x = ReportRuntimeResponse{}
|
*x = ReportRuntimeResponse{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[15]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[16]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1598,7 +1676,7 @@ func (x *ReportRuntimeResponse) String() string {
|
|||||||
func (*ReportRuntimeResponse) ProtoMessage() {}
|
func (*ReportRuntimeResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ReportRuntimeResponse) ProtoReflect() protoreflect.Message {
|
func (x *ReportRuntimeResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[15]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[16]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1611,7 +1689,7 @@ func (x *ReportRuntimeResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ReportRuntimeResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ReportRuntimeResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ReportRuntimeResponse) Descriptor() ([]byte, []int) {
|
func (*ReportRuntimeResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{15}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{16}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ReportRuntimeResponse) GetAcceptedOwnershipEpoch() uint64 {
|
func (x *ReportRuntimeResponse) GetAcceptedOwnershipEpoch() uint64 {
|
||||||
@ -1647,7 +1725,7 @@ type StreamCheckTasksRequest struct {
|
|||||||
|
|
||||||
func (x *StreamCheckTasksRequest) Reset() {
|
func (x *StreamCheckTasksRequest) Reset() {
|
||||||
*x = StreamCheckTasksRequest{}
|
*x = StreamCheckTasksRequest{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[16]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[17]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1659,7 +1737,7 @@ func (x *StreamCheckTasksRequest) String() string {
|
|||||||
func (*StreamCheckTasksRequest) ProtoMessage() {}
|
func (*StreamCheckTasksRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *StreamCheckTasksRequest) ProtoReflect() protoreflect.Message {
|
func (x *StreamCheckTasksRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[16]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[17]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1672,7 +1750,7 @@ func (x *StreamCheckTasksRequest) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use StreamCheckTasksRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use StreamCheckTasksRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*StreamCheckTasksRequest) Descriptor() ([]byte, []int) {
|
func (*StreamCheckTasksRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{16}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{17}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *StreamCheckTasksRequest) GetCheckerId() string {
|
func (x *StreamCheckTasksRequest) GetCheckerId() string {
|
||||||
@ -1723,7 +1801,7 @@ type CheckTask struct {
|
|||||||
|
|
||||||
func (x *CheckTask) Reset() {
|
func (x *CheckTask) Reset() {
|
||||||
*x = CheckTask{}
|
*x = CheckTask{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[17]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[18]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1735,7 +1813,7 @@ func (x *CheckTask) String() string {
|
|||||||
func (*CheckTask) ProtoMessage() {}
|
func (*CheckTask) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *CheckTask) ProtoReflect() protoreflect.Message {
|
func (x *CheckTask) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[17]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[18]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1748,7 +1826,7 @@ func (x *CheckTask) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use CheckTask.ProtoReflect.Descriptor instead.
|
// Deprecated: Use CheckTask.ProtoReflect.Descriptor instead.
|
||||||
func (*CheckTask) Descriptor() ([]byte, []int) {
|
func (*CheckTask) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{17}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{18}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CheckTask) GetTaskId() string {
|
func (x *CheckTask) GetTaskId() string {
|
||||||
@ -1845,7 +1923,7 @@ type ObservationBatch struct {
|
|||||||
|
|
||||||
func (x *ObservationBatch) Reset() {
|
func (x *ObservationBatch) Reset() {
|
||||||
*x = ObservationBatch{}
|
*x = ObservationBatch{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[18]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[19]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1857,7 +1935,7 @@ func (x *ObservationBatch) String() string {
|
|||||||
func (*ObservationBatch) ProtoMessage() {}
|
func (*ObservationBatch) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ObservationBatch) ProtoReflect() protoreflect.Message {
|
func (x *ObservationBatch) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[18]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[19]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1870,7 +1948,7 @@ func (x *ObservationBatch) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ObservationBatch.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ObservationBatch.ProtoReflect.Descriptor instead.
|
||||||
func (*ObservationBatch) Descriptor() ([]byte, []int) {
|
func (*ObservationBatch) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{18}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{19}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ObservationBatch) GetCheckerId() string {
|
func (x *ObservationBatch) GetCheckerId() string {
|
||||||
@ -1905,7 +1983,7 @@ type HealthObservation struct {
|
|||||||
|
|
||||||
func (x *HealthObservation) Reset() {
|
func (x *HealthObservation) Reset() {
|
||||||
*x = HealthObservation{}
|
*x = HealthObservation{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[19]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[20]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -1917,7 +1995,7 @@ func (x *HealthObservation) String() string {
|
|||||||
func (*HealthObservation) ProtoMessage() {}
|
func (*HealthObservation) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *HealthObservation) ProtoReflect() protoreflect.Message {
|
func (x *HealthObservation) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[19]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[20]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -1930,7 +2008,7 @@ func (x *HealthObservation) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use HealthObservation.ProtoReflect.Descriptor instead.
|
// Deprecated: Use HealthObservation.ProtoReflect.Descriptor instead.
|
||||||
func (*HealthObservation) Descriptor() ([]byte, []int) {
|
func (*HealthObservation) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{19}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{20}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *HealthObservation) GetTaskId() string {
|
func (x *HealthObservation) GetTaskId() string {
|
||||||
@ -2013,7 +2091,7 @@ type ReportObservationsResponse struct {
|
|||||||
|
|
||||||
func (x *ReportObservationsResponse) Reset() {
|
func (x *ReportObservationsResponse) Reset() {
|
||||||
*x = ReportObservationsResponse{}
|
*x = ReportObservationsResponse{}
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[20]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[21]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@ -2025,7 +2103,7 @@ func (x *ReportObservationsResponse) String() string {
|
|||||||
func (*ReportObservationsResponse) ProtoMessage() {}
|
func (*ReportObservationsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ReportObservationsResponse) ProtoReflect() protoreflect.Message {
|
func (x *ReportObservationsResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_controlplane_v1_controlplane_proto_msgTypes[20]
|
mi := &file_controlplane_v1_controlplane_proto_msgTypes[21]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@ -2038,7 +2116,7 @@ func (x *ReportObservationsResponse) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use ReportObservationsResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ReportObservationsResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ReportObservationsResponse) Descriptor() ([]byte, []int) {
|
func (*ReportObservationsResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{20}
|
return file_controlplane_v1_controlplane_proto_rawDescGZIP(), []int{21}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ReportObservationsResponse) GetAccepted() uint32 {
|
func (x *ReportObservationsResponse) GetAccepted() uint32 {
|
||||||
@ -2086,7 +2164,7 @@ const file_controlplane_v1_controlplane_proto_rawDesc = "" +
|
|||||||
"\x10SnapshotEnvelope\x12@\n" +
|
"\x10SnapshotEnvelope\x12@\n" +
|
||||||
"\x04full\x18\x01 \x01(\v2*.proxy_pool.controlplane.v1.WorkerSnapshotH\x00R\x04full\x12A\n" +
|
"\x04full\x18\x01 \x01(\v2*.proxy_pool.controlplane.v1.WorkerSnapshotH\x00R\x04full\x12A\n" +
|
||||||
"\x05delta\x18\x02 \x01(\v2).proxy_pool.controlplane.v1.SnapshotDeltaH\x00R\x05deltaB\t\n" +
|
"\x05delta\x18\x02 \x01(\v2).proxy_pool.controlplane.v1.SnapshotDeltaH\x00R\x05deltaB\t\n" +
|
||||||
"\apayload\"\xf0\x02\n" +
|
"\apayload\"\xc2\x03\n" +
|
||||||
"\x0eWorkerSnapshot\x12\x18\n" +
|
"\x0eWorkerSnapshot\x12\x18\n" +
|
||||||
"\aversion\x18\x01 \x01(\x04R\aversion\x12'\n" +
|
"\aversion\x18\x01 \x01(\x04R\aversion\x12'\n" +
|
||||||
"\x0fownership_epoch\x18\x02 \x01(\x04R\x0eownershipEpoch\x12=\n" +
|
"\x0fownership_epoch\x18\x02 \x01(\x04R\x0eownershipEpoch\x12=\n" +
|
||||||
@ -2095,7 +2173,14 @@ const file_controlplane_v1_controlplane_proto_rawDesc = "" +
|
|||||||
"validUntil\x12\x1a\n" +
|
"validUntil\x12\x1a\n" +
|
||||||
"\bchecksum\x18\x05 \x01(\fR\bchecksum\x12A\n" +
|
"\bchecksum\x18\x05 \x01(\fR\bchecksum\x12A\n" +
|
||||||
"\arouting\x18\x06 \x03(\v2'.proxy_pool.controlplane.v1.RoutingRuleR\arouting\x12@\n" +
|
"\arouting\x18\x06 \x03(\v2'.proxy_pool.controlplane.v1.RoutingRuleR\arouting\x12@\n" +
|
||||||
"\aproxies\x18\a \x03(\v2&.proxy_pool.controlplane.v1.OwnedProxyR\aproxies\"\xd7\x03\n" +
|
"\aproxies\x18\a \x03(\v2&.proxy_pool.controlplane.v1.OwnedProxyR\aproxies\x12P\n" +
|
||||||
|
"\vcredentials\x18\x0e \x03(\v2..proxy_pool.controlplane.v1.SnapshotCredentialR\vcredentials\"\x9a\x01\n" +
|
||||||
|
"\x12SnapshotCredential\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"secret_ref\x18\x01 \x01(\tR\tsecretRef\x12-\n" +
|
||||||
|
"\x12credential_version\x18\x02 \x01(\tR\x11credentialVersion\x12\x1a\n" +
|
||||||
|
"\busername\x18\x03 \x01(\tR\busername\x12\x1a\n" +
|
||||||
|
"\bpassword\x18\x04 \x01(\tR\bpassword\"\xd7\x03\n" +
|
||||||
"\rSnapshotDelta\x12!\n" +
|
"\rSnapshotDelta\x12!\n" +
|
||||||
"\fbase_version\x18\x01 \x01(\x04R\vbaseVersion\x12\x18\n" +
|
"\fbase_version\x18\x01 \x01(\x04R\vbaseVersion\x12\x18\n" +
|
||||||
"\aversion\x18\x02 \x01(\x04R\aversion\x12'\n" +
|
"\aversion\x18\x02 \x01(\x04R\aversion\x12'\n" +
|
||||||
@ -2295,7 +2380,7 @@ func file_controlplane_v1_controlplane_proto_rawDescGZIP() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file_controlplane_v1_controlplane_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
var file_controlplane_v1_controlplane_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||||
var file_controlplane_v1_controlplane_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
|
var file_controlplane_v1_controlplane_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
|
||||||
var file_controlplane_v1_controlplane_proto_goTypes = []any{
|
var file_controlplane_v1_controlplane_proto_goTypes = []any{
|
||||||
(StrategyType)(0), // 0: proxy_pool.controlplane.v1.StrategyType
|
(StrategyType)(0), // 0: proxy_pool.controlplane.v1.StrategyType
|
||||||
(UnavailableAction)(0), // 1: proxy_pool.controlplane.v1.UnavailableAction
|
(UnavailableAction)(0), // 1: proxy_pool.controlplane.v1.UnavailableAction
|
||||||
@ -2307,87 +2392,89 @@ var file_controlplane_v1_controlplane_proto_goTypes = []any{
|
|||||||
(*WatchSnapshotsRequest)(nil), // 7: proxy_pool.controlplane.v1.WatchSnapshotsRequest
|
(*WatchSnapshotsRequest)(nil), // 7: proxy_pool.controlplane.v1.WatchSnapshotsRequest
|
||||||
(*SnapshotEnvelope)(nil), // 8: proxy_pool.controlplane.v1.SnapshotEnvelope
|
(*SnapshotEnvelope)(nil), // 8: proxy_pool.controlplane.v1.SnapshotEnvelope
|
||||||
(*WorkerSnapshot)(nil), // 9: proxy_pool.controlplane.v1.WorkerSnapshot
|
(*WorkerSnapshot)(nil), // 9: proxy_pool.controlplane.v1.WorkerSnapshot
|
||||||
(*SnapshotDelta)(nil), // 10: proxy_pool.controlplane.v1.SnapshotDelta
|
(*SnapshotCredential)(nil), // 10: proxy_pool.controlplane.v1.SnapshotCredential
|
||||||
(*RoutingRule)(nil), // 11: proxy_pool.controlplane.v1.RoutingRule
|
(*SnapshotDelta)(nil), // 11: proxy_pool.controlplane.v1.SnapshotDelta
|
||||||
(*RoutingStrategy)(nil), // 12: proxy_pool.controlplane.v1.RoutingStrategy
|
(*RoutingRule)(nil), // 12: proxy_pool.controlplane.v1.RoutingRule
|
||||||
(*OwnedProxy)(nil), // 13: proxy_pool.controlplane.v1.OwnedProxy
|
(*RoutingStrategy)(nil), // 13: proxy_pool.controlplane.v1.RoutingStrategy
|
||||||
(*AcknowledgeSnapshotRequest)(nil), // 14: proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest
|
(*OwnedProxy)(nil), // 14: proxy_pool.controlplane.v1.OwnedProxy
|
||||||
(*OutcomeBatch)(nil), // 15: proxy_pool.controlplane.v1.OutcomeBatch
|
(*AcknowledgeSnapshotRequest)(nil), // 15: proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest
|
||||||
(*ProxyOutcome)(nil), // 16: proxy_pool.controlplane.v1.ProxyOutcome
|
(*OutcomeBatch)(nil), // 16: proxy_pool.controlplane.v1.OutcomeBatch
|
||||||
(*ReportOutcomesResponse)(nil), // 17: proxy_pool.controlplane.v1.ReportOutcomesResponse
|
(*ProxyOutcome)(nil), // 17: proxy_pool.controlplane.v1.ProxyOutcome
|
||||||
(*ReportRuntimeRequest)(nil), // 18: proxy_pool.controlplane.v1.ReportRuntimeRequest
|
(*ReportOutcomesResponse)(nil), // 18: proxy_pool.controlplane.v1.ReportOutcomesResponse
|
||||||
(*ProxyRuntime)(nil), // 19: proxy_pool.controlplane.v1.ProxyRuntime
|
(*ReportRuntimeRequest)(nil), // 19: proxy_pool.controlplane.v1.ReportRuntimeRequest
|
||||||
(*ReportRuntimeResponse)(nil), // 20: proxy_pool.controlplane.v1.ReportRuntimeResponse
|
(*ProxyRuntime)(nil), // 20: proxy_pool.controlplane.v1.ProxyRuntime
|
||||||
(*StreamCheckTasksRequest)(nil), // 21: proxy_pool.controlplane.v1.StreamCheckTasksRequest
|
(*ReportRuntimeResponse)(nil), // 21: proxy_pool.controlplane.v1.ReportRuntimeResponse
|
||||||
(*CheckTask)(nil), // 22: proxy_pool.controlplane.v1.CheckTask
|
(*StreamCheckTasksRequest)(nil), // 22: proxy_pool.controlplane.v1.StreamCheckTasksRequest
|
||||||
(*ObservationBatch)(nil), // 23: proxy_pool.controlplane.v1.ObservationBatch
|
(*CheckTask)(nil), // 23: proxy_pool.controlplane.v1.CheckTask
|
||||||
(*HealthObservation)(nil), // 24: proxy_pool.controlplane.v1.HealthObservation
|
(*ObservationBatch)(nil), // 24: proxy_pool.controlplane.v1.ObservationBatch
|
||||||
(*ReportObservationsResponse)(nil), // 25: proxy_pool.controlplane.v1.ReportObservationsResponse
|
(*HealthObservation)(nil), // 25: proxy_pool.controlplane.v1.HealthObservation
|
||||||
nil, // 26: proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry
|
(*ReportObservationsResponse)(nil), // 26: proxy_pool.controlplane.v1.ReportObservationsResponse
|
||||||
nil, // 27: proxy_pool.controlplane.v1.RoutingRule.HeadersEntry
|
nil, // 27: proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry
|
||||||
nil, // 28: proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry
|
nil, // 28: proxy_pool.controlplane.v1.RoutingRule.HeadersEntry
|
||||||
nil, // 29: proxy_pool.controlplane.v1.OwnedProxy.TagsEntry
|
nil, // 29: proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry
|
||||||
(*durationpb.Duration)(nil), // 30: google.protobuf.Duration
|
nil, // 30: proxy_pool.controlplane.v1.OwnedProxy.TagsEntry
|
||||||
(*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp
|
(*durationpb.Duration)(nil), // 31: google.protobuf.Duration
|
||||||
(*emptypb.Empty)(nil), // 32: google.protobuf.Empty
|
(*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp
|
||||||
|
(*emptypb.Empty)(nil), // 33: google.protobuf.Empty
|
||||||
}
|
}
|
||||||
var file_controlplane_v1_controlplane_proto_depIdxs = []int32{
|
var file_controlplane_v1_controlplane_proto_depIdxs = []int32{
|
||||||
26, // 0: proxy_pool.controlplane.v1.RegisterWorkerRequest.labels:type_name -> proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry
|
27, // 0: proxy_pool.controlplane.v1.RegisterWorkerRequest.labels:type_name -> proxy_pool.controlplane.v1.RegisterWorkerRequest.LabelsEntry
|
||||||
30, // 1: proxy_pool.controlplane.v1.RegisterWorkerResponse.heartbeat_interval:type_name -> google.protobuf.Duration
|
31, // 1: proxy_pool.controlplane.v1.RegisterWorkerResponse.heartbeat_interval:type_name -> google.protobuf.Duration
|
||||||
30, // 2: proxy_pool.controlplane.v1.RegisterWorkerResponse.max_stale_age:type_name -> google.protobuf.Duration
|
31, // 2: proxy_pool.controlplane.v1.RegisterWorkerResponse.max_stale_age:type_name -> google.protobuf.Duration
|
||||||
9, // 3: proxy_pool.controlplane.v1.SnapshotEnvelope.full:type_name -> proxy_pool.controlplane.v1.WorkerSnapshot
|
9, // 3: proxy_pool.controlplane.v1.SnapshotEnvelope.full:type_name -> proxy_pool.controlplane.v1.WorkerSnapshot
|
||||||
10, // 4: proxy_pool.controlplane.v1.SnapshotEnvelope.delta:type_name -> proxy_pool.controlplane.v1.SnapshotDelta
|
11, // 4: proxy_pool.controlplane.v1.SnapshotEnvelope.delta:type_name -> proxy_pool.controlplane.v1.SnapshotDelta
|
||||||
31, // 5: proxy_pool.controlplane.v1.WorkerSnapshot.generated_at:type_name -> google.protobuf.Timestamp
|
32, // 5: proxy_pool.controlplane.v1.WorkerSnapshot.generated_at:type_name -> google.protobuf.Timestamp
|
||||||
31, // 6: proxy_pool.controlplane.v1.WorkerSnapshot.valid_until:type_name -> google.protobuf.Timestamp
|
32, // 6: proxy_pool.controlplane.v1.WorkerSnapshot.valid_until:type_name -> google.protobuf.Timestamp
|
||||||
11, // 7: proxy_pool.controlplane.v1.WorkerSnapshot.routing:type_name -> proxy_pool.controlplane.v1.RoutingRule
|
12, // 7: proxy_pool.controlplane.v1.WorkerSnapshot.routing:type_name -> proxy_pool.controlplane.v1.RoutingRule
|
||||||
13, // 8: proxy_pool.controlplane.v1.WorkerSnapshot.proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy
|
14, // 8: proxy_pool.controlplane.v1.WorkerSnapshot.proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy
|
||||||
31, // 9: proxy_pool.controlplane.v1.SnapshotDelta.generated_at:type_name -> google.protobuf.Timestamp
|
10, // 9: proxy_pool.controlplane.v1.WorkerSnapshot.credentials:type_name -> proxy_pool.controlplane.v1.SnapshotCredential
|
||||||
11, // 10: proxy_pool.controlplane.v1.SnapshotDelta.upserted_routing:type_name -> proxy_pool.controlplane.v1.RoutingRule
|
32, // 10: proxy_pool.controlplane.v1.SnapshotDelta.generated_at:type_name -> google.protobuf.Timestamp
|
||||||
13, // 11: proxy_pool.controlplane.v1.SnapshotDelta.upserted_proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy
|
12, // 11: proxy_pool.controlplane.v1.SnapshotDelta.upserted_routing:type_name -> proxy_pool.controlplane.v1.RoutingRule
|
||||||
27, // 12: proxy_pool.controlplane.v1.RoutingRule.headers:type_name -> proxy_pool.controlplane.v1.RoutingRule.HeadersEntry
|
14, // 12: proxy_pool.controlplane.v1.SnapshotDelta.upserted_proxies:type_name -> proxy_pool.controlplane.v1.OwnedProxy
|
||||||
12, // 13: proxy_pool.controlplane.v1.RoutingRule.strategy:type_name -> proxy_pool.controlplane.v1.RoutingStrategy
|
28, // 13: proxy_pool.controlplane.v1.RoutingRule.headers:type_name -> proxy_pool.controlplane.v1.RoutingRule.HeadersEntry
|
||||||
1, // 14: proxy_pool.controlplane.v1.RoutingRule.on_unavailable:type_name -> proxy_pool.controlplane.v1.UnavailableAction
|
13, // 14: proxy_pool.controlplane.v1.RoutingRule.strategy:type_name -> proxy_pool.controlplane.v1.RoutingStrategy
|
||||||
30, // 15: proxy_pool.controlplane.v1.RoutingRule.wait_timeout:type_name -> google.protobuf.Duration
|
1, // 15: proxy_pool.controlplane.v1.RoutingRule.on_unavailable:type_name -> proxy_pool.controlplane.v1.UnavailableAction
|
||||||
0, // 16: proxy_pool.controlplane.v1.RoutingStrategy.type:type_name -> proxy_pool.controlplane.v1.StrategyType
|
31, // 16: proxy_pool.controlplane.v1.RoutingRule.wait_timeout:type_name -> google.protobuf.Duration
|
||||||
28, // 17: proxy_pool.controlplane.v1.RoutingStrategy.weights:type_name -> proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry
|
0, // 17: proxy_pool.controlplane.v1.RoutingStrategy.type:type_name -> proxy_pool.controlplane.v1.StrategyType
|
||||||
2, // 18: proxy_pool.controlplane.v1.OwnedProxy.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol
|
29, // 18: proxy_pool.controlplane.v1.RoutingStrategy.weights:type_name -> proxy_pool.controlplane.v1.RoutingStrategy.WeightsEntry
|
||||||
31, // 19: proxy_pool.controlplane.v1.OwnedProxy.expires_at:type_name -> google.protobuf.Timestamp
|
2, // 19: proxy_pool.controlplane.v1.OwnedProxy.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol
|
||||||
29, // 20: proxy_pool.controlplane.v1.OwnedProxy.tags:type_name -> proxy_pool.controlplane.v1.OwnedProxy.TagsEntry
|
32, // 20: proxy_pool.controlplane.v1.OwnedProxy.expires_at:type_name -> google.protobuf.Timestamp
|
||||||
31, // 21: proxy_pool.controlplane.v1.OwnedProxy.usable_until:type_name -> google.protobuf.Timestamp
|
30, // 21: proxy_pool.controlplane.v1.OwnedProxy.tags:type_name -> proxy_pool.controlplane.v1.OwnedProxy.TagsEntry
|
||||||
16, // 22: proxy_pool.controlplane.v1.OutcomeBatch.outcomes:type_name -> proxy_pool.controlplane.v1.ProxyOutcome
|
32, // 22: proxy_pool.controlplane.v1.OwnedProxy.usable_until:type_name -> google.protobuf.Timestamp
|
||||||
3, // 23: proxy_pool.controlplane.v1.ProxyOutcome.stage:type_name -> proxy_pool.controlplane.v1.OutcomeStage
|
17, // 23: proxy_pool.controlplane.v1.OutcomeBatch.outcomes:type_name -> proxy_pool.controlplane.v1.ProxyOutcome
|
||||||
30, // 24: proxy_pool.controlplane.v1.ProxyOutcome.latency:type_name -> google.protobuf.Duration
|
3, // 24: proxy_pool.controlplane.v1.ProxyOutcome.stage:type_name -> proxy_pool.controlplane.v1.OutcomeStage
|
||||||
31, // 25: proxy_pool.controlplane.v1.ProxyOutcome.observed_at:type_name -> google.protobuf.Timestamp
|
31, // 25: proxy_pool.controlplane.v1.ProxyOutcome.latency:type_name -> google.protobuf.Duration
|
||||||
19, // 26: proxy_pool.controlplane.v1.ReportRuntimeRequest.counters:type_name -> proxy_pool.controlplane.v1.ProxyRuntime
|
32, // 26: proxy_pool.controlplane.v1.ProxyOutcome.observed_at:type_name -> google.protobuf.Timestamp
|
||||||
31, // 27: proxy_pool.controlplane.v1.ReportRuntimeRequest.observed_at:type_name -> google.protobuf.Timestamp
|
20, // 27: proxy_pool.controlplane.v1.ReportRuntimeRequest.counters:type_name -> proxy_pool.controlplane.v1.ProxyRuntime
|
||||||
4, // 28: proxy_pool.controlplane.v1.StreamCheckTasksRequest.supported_levels:type_name -> proxy_pool.controlplane.v1.CheckLevel
|
32, // 28: proxy_pool.controlplane.v1.ReportRuntimeRequest.observed_at:type_name -> google.protobuf.Timestamp
|
||||||
2, // 29: proxy_pool.controlplane.v1.CheckTask.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol
|
4, // 29: proxy_pool.controlplane.v1.StreamCheckTasksRequest.supported_levels:type_name -> proxy_pool.controlplane.v1.CheckLevel
|
||||||
4, // 30: proxy_pool.controlplane.v1.CheckTask.level:type_name -> proxy_pool.controlplane.v1.CheckLevel
|
2, // 30: proxy_pool.controlplane.v1.CheckTask.protocol:type_name -> proxy_pool.controlplane.v1.ProxyProtocol
|
||||||
30, // 31: proxy_pool.controlplane.v1.CheckTask.timeout:type_name -> google.protobuf.Duration
|
4, // 31: proxy_pool.controlplane.v1.CheckTask.level:type_name -> proxy_pool.controlplane.v1.CheckLevel
|
||||||
31, // 32: proxy_pool.controlplane.v1.CheckTask.deadline:type_name -> google.protobuf.Timestamp
|
31, // 32: proxy_pool.controlplane.v1.CheckTask.timeout:type_name -> google.protobuf.Duration
|
||||||
24, // 33: proxy_pool.controlplane.v1.ObservationBatch.observations:type_name -> proxy_pool.controlplane.v1.HealthObservation
|
32, // 33: proxy_pool.controlplane.v1.CheckTask.deadline:type_name -> google.protobuf.Timestamp
|
||||||
4, // 34: proxy_pool.controlplane.v1.HealthObservation.level:type_name -> proxy_pool.controlplane.v1.CheckLevel
|
25, // 34: proxy_pool.controlplane.v1.ObservationBatch.observations:type_name -> proxy_pool.controlplane.v1.HealthObservation
|
||||||
30, // 35: proxy_pool.controlplane.v1.HealthObservation.latency:type_name -> google.protobuf.Duration
|
4, // 35: proxy_pool.controlplane.v1.HealthObservation.level:type_name -> proxy_pool.controlplane.v1.CheckLevel
|
||||||
31, // 36: proxy_pool.controlplane.v1.HealthObservation.observed_at:type_name -> google.protobuf.Timestamp
|
31, // 36: proxy_pool.controlplane.v1.HealthObservation.latency:type_name -> google.protobuf.Duration
|
||||||
5, // 37: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:input_type -> proxy_pool.controlplane.v1.RegisterWorkerRequest
|
32, // 37: proxy_pool.controlplane.v1.HealthObservation.observed_at:type_name -> google.protobuf.Timestamp
|
||||||
7, // 38: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:input_type -> proxy_pool.controlplane.v1.WatchSnapshotsRequest
|
5, // 38: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:input_type -> proxy_pool.controlplane.v1.RegisterWorkerRequest
|
||||||
14, // 39: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:input_type -> proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest
|
7, // 39: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:input_type -> proxy_pool.controlplane.v1.WatchSnapshotsRequest
|
||||||
15, // 40: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:input_type -> proxy_pool.controlplane.v1.OutcomeBatch
|
15, // 40: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:input_type -> proxy_pool.controlplane.v1.AcknowledgeSnapshotRequest
|
||||||
18, // 41: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:input_type -> proxy_pool.controlplane.v1.ReportRuntimeRequest
|
16, // 41: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:input_type -> proxy_pool.controlplane.v1.OutcomeBatch
|
||||||
21, // 42: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:input_type -> proxy_pool.controlplane.v1.StreamCheckTasksRequest
|
19, // 42: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:input_type -> proxy_pool.controlplane.v1.ReportRuntimeRequest
|
||||||
23, // 43: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:input_type -> proxy_pool.controlplane.v1.ObservationBatch
|
22, // 43: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:input_type -> proxy_pool.controlplane.v1.StreamCheckTasksRequest
|
||||||
6, // 44: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:output_type -> proxy_pool.controlplane.v1.RegisterWorkerResponse
|
24, // 44: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:input_type -> proxy_pool.controlplane.v1.ObservationBatch
|
||||||
8, // 45: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:output_type -> proxy_pool.controlplane.v1.SnapshotEnvelope
|
6, // 45: proxy_pool.controlplane.v1.WorkerControlPlane.RegisterWorker:output_type -> proxy_pool.controlplane.v1.RegisterWorkerResponse
|
||||||
32, // 46: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:output_type -> google.protobuf.Empty
|
8, // 46: proxy_pool.controlplane.v1.WorkerControlPlane.WatchSnapshots:output_type -> proxy_pool.controlplane.v1.SnapshotEnvelope
|
||||||
17, // 47: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:output_type -> proxy_pool.controlplane.v1.ReportOutcomesResponse
|
33, // 47: proxy_pool.controlplane.v1.WorkerControlPlane.AcknowledgeSnapshot:output_type -> google.protobuf.Empty
|
||||||
20, // 48: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:output_type -> proxy_pool.controlplane.v1.ReportRuntimeResponse
|
18, // 48: proxy_pool.controlplane.v1.WorkerControlPlane.ReportOutcomes:output_type -> proxy_pool.controlplane.v1.ReportOutcomesResponse
|
||||||
22, // 49: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:output_type -> proxy_pool.controlplane.v1.CheckTask
|
21, // 49: proxy_pool.controlplane.v1.WorkerControlPlane.ReportRuntime:output_type -> proxy_pool.controlplane.v1.ReportRuntimeResponse
|
||||||
25, // 50: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:output_type -> proxy_pool.controlplane.v1.ReportObservationsResponse
|
23, // 50: proxy_pool.controlplane.v1.CheckerControlPlane.StreamCheckTasks:output_type -> proxy_pool.controlplane.v1.CheckTask
|
||||||
44, // [44:51] is the sub-list for method output_type
|
26, // 51: proxy_pool.controlplane.v1.CheckerControlPlane.ReportObservations:output_type -> proxy_pool.controlplane.v1.ReportObservationsResponse
|
||||||
37, // [37:44] is the sub-list for method input_type
|
45, // [45:52] is the sub-list for method output_type
|
||||||
37, // [37:37] is the sub-list for extension type_name
|
38, // [38:45] is the sub-list for method input_type
|
||||||
37, // [37:37] is the sub-list for extension extendee
|
38, // [38:38] is the sub-list for extension type_name
|
||||||
0, // [0:37] is the sub-list for field type_name
|
38, // [38:38] is the sub-list for extension extendee
|
||||||
|
0, // [0:38] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_controlplane_v1_controlplane_proto_init() }
|
func init() { file_controlplane_v1_controlplane_proto_init() }
|
||||||
@ -2405,7 +2492,7 @@ func file_controlplane_v1_controlplane_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_v1_controlplane_proto_rawDesc), len(file_controlplane_v1_controlplane_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_v1_controlplane_proto_rawDesc), len(file_controlplane_v1_controlplane_proto_rawDesc)),
|
||||||
NumEnums: 5,
|
NumEnums: 5,
|
||||||
NumMessages: 25,
|
NumMessages: 26,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 2,
|
NumServices: 2,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -139,6 +139,7 @@ type ControlPlane struct {
|
|||||||
MaxRuntimeCounters int `yaml:"maxRuntimeCounters"`
|
MaxRuntimeCounters int `yaml:"maxRuntimeCounters"`
|
||||||
MaxConcurrentStreams uint32 `yaml:"maxConcurrentStreams"`
|
MaxConcurrentStreams uint32 `yaml:"maxConcurrentStreams"`
|
||||||
TLS ControlPlaneTLS `yaml:"tls"`
|
TLS ControlPlaneTLS `yaml:"tls"`
|
||||||
|
GatewayTLS GatewayTLS `yaml:"gatewayTLS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ControlPlaneTLS struct {
|
type ControlPlaneTLS struct {
|
||||||
@ -150,6 +151,14 @@ type ControlPlaneTLS struct {
|
|||||||
Environment string `yaml:"environment"`
|
Environment string `yaml:"environment"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GatewayTLS holds client-only mTLS material. It remains separate from the
|
||||||
|
// Controller's server certificate and CA configuration.
|
||||||
|
type GatewayTLS struct {
|
||||||
|
CertFile string `yaml:"certFile"`
|
||||||
|
KeyFile string `yaml:"keyFile"`
|
||||||
|
ServerCAFile string `yaml:"serverCAFile"`
|
||||||
|
}
|
||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
PostgresURL string `yaml:"postgresURL"`
|
PostgresURL string `yaml:"postgresURL"`
|
||||||
RedisURL string `yaml:"redisURL"`
|
RedisURL string `yaml:"redisURL"`
|
||||||
|
|||||||
@ -454,6 +454,14 @@ func TestValidateControlPlane(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: "environment",
|
want: "environment",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "partial gateway tls",
|
||||||
|
mutate: func(cfg *Config) {
|
||||||
|
cfg.ControlPlane = validMTLSControlPlane()
|
||||||
|
cfg.ControlPlane.GatewayTLS.CertFile = "/run/secrets/gateway-cert.pem"
|
||||||
|
},
|
||||||
|
want: "gatewayTLS",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|||||||
@ -110,6 +110,9 @@ func Validate(cfg *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateControlPlane(item ControlPlane) error {
|
func validateControlPlane(item ControlPlane) error {
|
||||||
|
if err := validateGatewayTLS(item.GatewayTLS); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if !item.Enabled {
|
if !item.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -161,6 +164,17 @@ func validateControlPlane(item ControlPlane) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateGatewayTLS(item GatewayTLS) error {
|
||||||
|
configured := item.CertFile != "" || item.KeyFile != "" || item.ServerCAFile != ""
|
||||||
|
if !configured {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if item.CertFile == "" || item.KeyFile == "" || item.ServerCAFile == "" {
|
||||||
|
return fmt.Errorf("validate controlPlane gatewayTLS: certFile, keyFile and serverCAFile are required together")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validTrustDomain(value string) bool {
|
func validTrustDomain(value string) bool {
|
||||||
if len(value) == 0 || len(value) > 253 {
|
if len(value) == 0 || len(value) > 253 {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -257,6 +257,7 @@ func runWithWorkerFactory(
|
|||||||
MaxSnapshotBytes: loaded.Value.ControlPlane.MaxMessageBytes,
|
MaxSnapshotBytes: loaded.Value.ControlPlane.MaxMessageBytes,
|
||||||
SnapshotReader: snapshotReader,
|
SnapshotReader: snapshotReader,
|
||||||
RoutingSource: routingSource,
|
RoutingSource: routingSource,
|
||||||
|
Credentials: opened.credentials,
|
||||||
})
|
})
|
||||||
if serviceErr != nil {
|
if serviceErr != nil {
|
||||||
return fmt.Errorf("%w: build Worker control service: %w", ErrStartup, serviceErr)
|
return fmt.Errorf("%w: build Worker control service: %w", ErrStartup, serviceErr)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
controlplanev1 "proxy-pool/gen/controlplane/v1"
|
controlplanev1 "proxy-pool/gen/controlplane/v1"
|
||||||
@ -11,6 +12,7 @@ import (
|
|||||||
ownershipDomain "proxy-pool/internal/domain/ownership"
|
ownershipDomain "proxy-pool/internal/domain/ownership"
|
||||||
proxyDomain "proxy-pool/internal/domain/proxy"
|
proxyDomain "proxy-pool/internal/domain/proxy"
|
||||||
"proxy-pool/internal/domain/workerruntime"
|
"proxy-pool/internal/domain/workerruntime"
|
||||||
|
platformCredentials "proxy-pool/internal/platform/credentials"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
@ -28,6 +30,11 @@ type OwnedSnapshotSource struct {
|
|||||||
maxBytes int
|
maxBytes int
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
routing RoutingSource
|
routing RoutingSource
|
||||||
|
credentials CredentialReader
|
||||||
|
}
|
||||||
|
|
||||||
|
type CredentialReader interface {
|
||||||
|
Resolve(context.Context, platformCredentials.Reference) (platformCredentials.Value, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOwnedSnapshotSource(
|
func NewOwnedSnapshotSource(
|
||||||
@ -38,6 +45,35 @@ func NewOwnedSnapshotSource(
|
|||||||
maxBytes int,
|
maxBytes int,
|
||||||
now func() time.Time,
|
now func() time.Time,
|
||||||
routing ...RoutingSource,
|
routing ...RoutingSource,
|
||||||
|
) (*OwnedSnapshotSource, error) {
|
||||||
|
return newOwnedSnapshotSource(epochs, reader, validFor, maxProxies, maxBytes, now, nil, routing...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOwnedSnapshotSourceWithCredentials(
|
||||||
|
epochs OwnershipEpochReader,
|
||||||
|
reader ownershipDomain.SnapshotReader,
|
||||||
|
validFor time.Duration,
|
||||||
|
maxProxies int,
|
||||||
|
maxBytes int,
|
||||||
|
now func() time.Time,
|
||||||
|
credentials CredentialReader,
|
||||||
|
routing ...RoutingSource,
|
||||||
|
) (*OwnedSnapshotSource, error) {
|
||||||
|
if credentials == nil {
|
||||||
|
return nil, ErrSnapshotsUnavailable
|
||||||
|
}
|
||||||
|
return newOwnedSnapshotSource(epochs, reader, validFor, maxProxies, maxBytes, now, credentials, routing...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOwnedSnapshotSource(
|
||||||
|
epochs OwnershipEpochReader,
|
||||||
|
reader ownershipDomain.SnapshotReader,
|
||||||
|
validFor time.Duration,
|
||||||
|
maxProxies int,
|
||||||
|
maxBytes int,
|
||||||
|
now func() time.Time,
|
||||||
|
credentials CredentialReader,
|
||||||
|
routing ...RoutingSource,
|
||||||
) (*OwnedSnapshotSource, error) {
|
) (*OwnedSnapshotSource, error) {
|
||||||
if epochs == nil || reader == nil || validFor <= 0 || maxProxies <= 0 || maxBytes <= 0 || now == nil || len(routing) > 1 ||
|
if epochs == nil || reader == nil || validFor <= 0 || maxProxies <= 0 || maxBytes <= 0 || now == nil || len(routing) > 1 ||
|
||||||
len(routing) == 1 && routing[0] == nil {
|
len(routing) == 1 && routing[0] == nil {
|
||||||
@ -45,6 +81,7 @@ func NewOwnedSnapshotSource(
|
|||||||
}
|
}
|
||||||
source := &OwnedSnapshotSource{
|
source := &OwnedSnapshotSource{
|
||||||
epochs: epochs, reader: reader, validFor: validFor, maxProxies: maxProxies, maxBytes: maxBytes, now: now,
|
epochs: epochs, reader: reader, validFor: validFor, maxProxies: maxProxies, maxBytes: maxBytes, now: now,
|
||||||
|
credentials: credentials,
|
||||||
}
|
}
|
||||||
if len(routing) == 1 {
|
if len(routing) == 1 {
|
||||||
source.routing = routing[0]
|
source.routing = routing[0]
|
||||||
@ -77,6 +114,7 @@ func (source *OwnedSnapshotSource) Watch(ctx context.Context, request SnapshotWa
|
|||||||
GeneratedAt: timestamppb.New(now), ValidUntil: timestamppb.New(now.Add(source.validFor)),
|
GeneratedAt: timestamppb.New(now), ValidUntil: timestamppb.New(now.Add(source.validFor)),
|
||||||
Proxies: make([]*controlplanev1.OwnedProxy, 0, len(proxies)),
|
Proxies: make([]*controlplanev1.OwnedProxy, 0, len(proxies)),
|
||||||
}
|
}
|
||||||
|
materials := make(map[platformCredentials.Reference]platformCredentials.Value)
|
||||||
for _, item := range proxies {
|
for _, item := range proxies {
|
||||||
owned, validUntil, err := wireOwnedProxy(item, now)
|
owned, validUntil, err := wireOwnedProxy(item, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -86,7 +124,26 @@ func (source *OwnedSnapshotSource) Watch(ctx context.Context, request SnapshotWa
|
|||||||
full.ValidUntil = timestamppb.New(validUntil)
|
full.ValidUntil = timestamppb.New(validUntil)
|
||||||
}
|
}
|
||||||
full.Proxies = append(full.Proxies, owned)
|
full.Proxies = append(full.Proxies, owned)
|
||||||
|
if owned.GetSecretRef() != "" {
|
||||||
|
reference := platformCredentials.Reference{SecretRef: owned.GetSecretRef(), CredentialVersion: owned.GetCredentialVersion()}
|
||||||
|
if source.credentials == nil {
|
||||||
|
return nil, ErrSnapshotCredentialsUnavailable
|
||||||
}
|
}
|
||||||
|
material, resolveErr := source.credentials.Resolve(ctx, reference)
|
||||||
|
if resolveErr != nil || (material.Username == "" && material.Password == "") ||
|
||||||
|
(owned.GetUsername() != "" && material.Username != "" && material.Username != owned.GetUsername()) {
|
||||||
|
return nil, ErrSnapshotCredentialsUnavailable
|
||||||
|
}
|
||||||
|
if material.Username == "" {
|
||||||
|
material.Username = owned.GetUsername()
|
||||||
|
}
|
||||||
|
if existing, exists := materials[reference]; exists && existing != material {
|
||||||
|
return nil, ErrSnapshotCredentialsUnavailable
|
||||||
|
}
|
||||||
|
materials[reference] = material
|
||||||
|
}
|
||||||
|
}
|
||||||
|
full.Credentials = wireSnapshotCredentials(materials)
|
||||||
if source.routing != nil {
|
if source.routing != nil {
|
||||||
routing, err := source.routing.Read(ctx)
|
routing, err := source.routing.Read(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -107,6 +164,23 @@ func (source *OwnedSnapshotSource) Watch(ctx context.Context, request SnapshotWa
|
|||||||
return updates, nil
|
return updates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wireSnapshotCredentials(materials map[platformCredentials.Reference]platformCredentials.Value) []*controlplanev1.SnapshotCredential {
|
||||||
|
result := make([]*controlplanev1.SnapshotCredential, 0, len(materials))
|
||||||
|
for reference, material := range materials {
|
||||||
|
result = append(result, &controlplanev1.SnapshotCredential{
|
||||||
|
SecretRef: reference.SecretRef, CredentialVersion: reference.CredentialVersion,
|
||||||
|
Username: material.Username, Password: material.Password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(left, right int) bool {
|
||||||
|
if result[left].GetSecretRef() == result[right].GetSecretRef() {
|
||||||
|
return result[left].GetCredentialVersion() < result[right].GetCredentialVersion()
|
||||||
|
}
|
||||||
|
return result[left].GetSecretRef() < result[right].GetSecretRef()
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func cloneSnapshotRouting(source []*controlplanev1.RoutingRule) []*controlplanev1.RoutingRule {
|
func cloneSnapshotRouting(source []*controlplanev1.RoutingRule) []*controlplanev1.RoutingRule {
|
||||||
result := make([]*controlplanev1.RoutingRule, len(source))
|
result := make([]*controlplanev1.RoutingRule, len(source))
|
||||||
for index, rule := range source {
|
for index, rule := range source {
|
||||||
@ -119,7 +193,7 @@ func cloneSnapshotRouting(source []*controlplanev1.RoutingRule) []*controlplanev
|
|||||||
|
|
||||||
func wireOwnedProxy(item ownershipDomain.SnapshotProxy, now time.Time) (*controlplanev1.OwnedProxy, time.Time, error) {
|
func wireOwnedProxy(item ownershipDomain.SnapshotProxy, now time.Time) (*controlplanev1.OwnedProxy, time.Time, error) {
|
||||||
proxy := item.Proxy
|
proxy := item.Proxy
|
||||||
if item.OwnershipEpoch == 0 || item.LeaseExpiresAt.IsZero() || proxy.CredentialVersion != "" || proxy.SecretRef != "" {
|
if item.OwnershipEpoch == 0 || item.LeaseExpiresAt.IsZero() || (proxy.CredentialVersion == "") != (proxy.SecretRef == "") {
|
||||||
if proxy.CredentialVersion != "" || proxy.SecretRef != "" {
|
if proxy.CredentialVersion != "" || proxy.SecretRef != "" {
|
||||||
return nil, time.Time{}, ErrSnapshotCredentialsUnavailable
|
return nil, time.Time{}, ErrSnapshotCredentialsUnavailable
|
||||||
}
|
}
|
||||||
@ -144,7 +218,7 @@ func wireOwnedProxy(item ownershipDomain.SnapshotProxy, now time.Time) (*control
|
|||||||
}
|
}
|
||||||
return &controlplanev1.OwnedProxy{
|
return &controlplanev1.OwnedProxy{
|
||||||
Id: proxy.ID, Upstream: proxy.SourceUpstream, Protocol: protocol, Host: proxy.Host, Port: uint32(proxy.Port),
|
Id: proxy.ID, Upstream: proxy.SourceUpstream, Protocol: protocol, Host: proxy.Host, Port: uint32(proxy.Port),
|
||||||
Username: proxy.Username, ExpiresAt: timestamppb.New(proxy.ExpiresAt.UTC()),
|
Username: proxy.Username, CredentialVersion: proxy.CredentialVersion, SecretRef: proxy.SecretRef, ExpiresAt: timestamppb.New(proxy.ExpiresAt.UTC()),
|
||||||
MaxConcurrency: uint32(proxy.MaxConcurrency), Tags: tags, OwnershipEpoch: item.OwnershipEpoch,
|
MaxConcurrency: uint32(proxy.MaxConcurrency), Tags: tags, OwnershipEpoch: item.OwnershipEpoch,
|
||||||
UsableUntil: timestamppb.New(validUntil),
|
UsableUntil: timestamppb.New(validUntil),
|
||||||
}, validUntil, nil
|
}, validUntil, nil
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
ownershipDomain "proxy-pool/internal/domain/ownership"
|
ownershipDomain "proxy-pool/internal/domain/ownership"
|
||||||
"proxy-pool/internal/domain/workerruntime"
|
"proxy-pool/internal/domain/workerruntime"
|
||||||
|
platformCredentials "proxy-pool/internal/platform/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -59,6 +60,7 @@ type Options struct {
|
|||||||
MaxSnapshotBytes int
|
MaxSnapshotBytes int
|
||||||
SnapshotReader ownershipDomain.SnapshotReader
|
SnapshotReader ownershipDomain.SnapshotReader
|
||||||
RoutingSource RoutingSource
|
RoutingSource RoutingSource
|
||||||
|
Credentials platformCredentials.Store
|
||||||
SessionID func() (string, error)
|
SessionID func() (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +138,17 @@ func NewService(store workerruntime.ControlStore, options Options) (Service, err
|
|||||||
if options.RoutingSource != nil {
|
if options.RoutingSource != nil {
|
||||||
routing = append(routing, options.RoutingSource)
|
routing = append(routing, options.RoutingSource)
|
||||||
}
|
}
|
||||||
source, err := NewOwnedSnapshotSource(
|
var source SnapshotSource
|
||||||
|
var err error
|
||||||
|
if options.Credentials != nil {
|
||||||
|
source, err = NewOwnedSnapshotSourceWithCredentials(
|
||||||
|
result, options.SnapshotReader, options.MaxStaleAge, options.MaxRuntimeCounters, options.MaxSnapshotBytes, time.Now, options.Credentials, routing...,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
source, err = NewOwnedSnapshotSource(
|
||||||
result, options.SnapshotReader, options.MaxStaleAge, options.MaxRuntimeCounters, options.MaxSnapshotBytes, time.Now, routing...,
|
result, options.SnapshotReader, options.MaxStaleAge, options.MaxRuntimeCounters, options.MaxSnapshotBytes, time.Now, routing...,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Join(ErrInvalidCommand, err)
|
return nil, errors.Join(ErrInvalidCommand, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"proxy-pool/internal/controlplane/snapshotwire"
|
"proxy-pool/internal/controlplane/snapshotwire"
|
||||||
ownershipDomain "proxy-pool/internal/domain/ownership"
|
ownershipDomain "proxy-pool/internal/domain/ownership"
|
||||||
proxyDomain "proxy-pool/internal/domain/proxy"
|
proxyDomain "proxy-pool/internal/domain/proxy"
|
||||||
|
platformCredentials "proxy-pool/internal/platform/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInitialSnapshotSourceIssuesNextFullSnapshot(t *testing.T) {
|
func TestInitialSnapshotSourceIssuesNextFullSnapshot(t *testing.T) {
|
||||||
@ -123,6 +124,44 @@ func TestOwnedSnapshotSourceRejectsCredentialReferenceUntilMaterialIsAvailable(t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOwnedSnapshotSourceIncludesReferencedCredentialMaterial(t *testing.T) {
|
||||||
|
now := time.Date(2026, 7, 31, 12, 0, 0, 0, time.UTC)
|
||||||
|
expiresAt := now.Add(time.Minute)
|
||||||
|
usableUntil := now.Add(30 * time.Second)
|
||||||
|
store, err := platformCredentials.NewMemoryStore(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewMemoryStore(): %v", err)
|
||||||
|
}
|
||||||
|
reference, err := store.Put(context.Background(), "provider-a", platformCredentials.Value{Username: "upstream", Password: "secret"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Put(): %v", err)
|
||||||
|
}
|
||||||
|
source, err := NewOwnedSnapshotSourceWithCredentials(epochReaderStub{epoch: 1}, snapshotReaderStub{proxies: []ownershipDomain.SnapshotProxy{{
|
||||||
|
Proxy: proxyDomain.Proxy{
|
||||||
|
ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080, SourceUpstream: "upstream-a",
|
||||||
|
Username: "upstream", SecretRef: reference.SecretRef, CredentialVersion: reference.CredentialVersion,
|
||||||
|
ExpiresAt: &expiresAt, UsableUntil: &usableUntil, MaxConcurrency: 1, State: proxyDomain.StateAvailable,
|
||||||
|
}, OwnershipEpoch: 1, LeaseExpiresAt: now.Add(time.Minute),
|
||||||
|
}}}, time.Minute, 10, 4096, func() time.Time { return now }, store)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewOwnedSnapshotSourceWithCredentials(): %v", err)
|
||||||
|
}
|
||||||
|
updates, err := source.Watch(context.Background(), SnapshotWatchRequest{WorkerID: "worker-a", SessionID: "session-a"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Watch(): %v", err)
|
||||||
|
}
|
||||||
|
full := <-updates
|
||||||
|
if len(full.GetCredentials()) != 1 || full.GetCredentials()[0].GetSecretRef() != reference.SecretRef ||
|
||||||
|
full.GetCredentials()[0].GetUsername() != "upstream" || full.GetCredentials()[0].GetPassword() != "secret" ||
|
||||||
|
full.GetProxies()[0].GetCredentialVersion() != reference.CredentialVersion {
|
||||||
|
t.Fatal("snapshot credential material or proxy credential reference is invalid")
|
||||||
|
}
|
||||||
|
checksum, err := snapshotwire.Checksum(full)
|
||||||
|
if err != nil || string(checksum[:]) != string(full.GetChecksum()) {
|
||||||
|
t.Fatalf("snapshot checksum = %x, %v; want %x", full.GetChecksum(), err, checksum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type epochReaderStub struct {
|
type epochReaderStub struct {
|
||||||
epoch uint64
|
epoch uint64
|
||||||
err error
|
err error
|
||||||
|
|||||||
465
internal/gateway/bootstrap/bootstrap.go
Normal file
465
internal/gateway/bootstrap/bootstrap.go
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
// Package bootstrap assembles the proxy-gateway process from data-plane
|
||||||
|
// components without putting Controller or storage access on the request path.
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
|
controlplanev1 "proxy-pool/gen/controlplane/v1"
|
||||||
|
"proxy-pool/internal/config"
|
||||||
|
proxyDomain "proxy-pool/internal/domain/proxy"
|
||||||
|
"proxy-pool/internal/domain/workerruntime"
|
||||||
|
"proxy-pool/internal/gateway/controlplane"
|
||||||
|
"proxy-pool/internal/gateway/dispatch"
|
||||||
|
"proxy-pool/internal/gateway/server"
|
||||||
|
"proxy-pool/internal/gateway/snapshot"
|
||||||
|
"proxy-pool/internal/gateway/transport"
|
||||||
|
"proxy-pool/internal/platform/httpserver"
|
||||||
|
"proxy-pool/internal/platform/lifecycle"
|
||||||
|
platformMetrics "proxy-pool/internal/platform/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidOptions = errors.New("invalid gateway bootstrap options")
|
||||||
|
ErrStartup = errors.New("gateway startup failed")
|
||||||
|
ErrNotReady = errors.New("gateway snapshot is not ready")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultReconnectInitialDelay = time.Second
|
||||||
|
defaultReconnectMaxDelay = 30 * time.Second
|
||||||
|
defaultReconnectJitter = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options provides process-local settings. ControlPlaneAddress is deliberately
|
||||||
|
// independent from controlPlane.listen: the latter is the Controller bind
|
||||||
|
// address, while this value is the Gateway's remote dial target.
|
||||||
|
type Options struct {
|
||||||
|
ConfigPath string
|
||||||
|
Resolver config.Resolver
|
||||||
|
ControlPlaneAddress string
|
||||||
|
ClusterID string
|
||||||
|
WorkerID string
|
||||||
|
InstanceID string
|
||||||
|
Zone string
|
||||||
|
Labels map[string]string
|
||||||
|
HTTP httpserver.Options
|
||||||
|
|
||||||
|
// The pre-bound listeners and transport are test seams. Production leaves
|
||||||
|
// them nil and derives listeners and mTLS credentials from configuration.
|
||||||
|
GatewayListener net.Listener
|
||||||
|
MetricsListener net.Listener
|
||||||
|
GRPCTransport credentials.TransportCredentials
|
||||||
|
|
||||||
|
ReconnectInitialDelay time.Duration
|
||||||
|
ReconnectMaxDelay time.Duration
|
||||||
|
ReconnectJitter int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(ctx context.Context, options Options) error {
|
||||||
|
if err := validateOptions(ctx, options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configuration, err := loadConfiguration(ctx, options.ConfigPath, options.Resolver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: load configuration: %w", ErrStartup, err)
|
||||||
|
}
|
||||||
|
if !configuration.Gateway.Enabled || !configuration.ControlPlane.Enabled {
|
||||||
|
return errors.Join(ErrInvalidOptions, errors.New("gateway and controlPlane must be enabled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime, err := newRuntime(ctx, configuration, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrStartup, err)
|
||||||
|
}
|
||||||
|
defer runtime.Close()
|
||||||
|
return runtime.Run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOptions(ctx context.Context, options Options) error {
|
||||||
|
if ctx == nil || strings.TrimSpace(options.ConfigPath) != options.ConfigPath || options.ConfigPath == "" ||
|
||||||
|
nilInterface(options.Resolver) || !workerruntime.ValidIdentifier(options.ClusterID) || !workerruntime.ValidIdentifier(options.WorkerID) ||
|
||||||
|
!workerruntime.ValidIdentifier(options.InstanceID) || !workerruntime.ValidIdentifier(options.Zone) || !validDialAddress(options.ControlPlaneAddress) {
|
||||||
|
return ErrInvalidOptions
|
||||||
|
}
|
||||||
|
if options.GatewayListener == nil && options.MetricsListener != nil {
|
||||||
|
return ErrInvalidOptions
|
||||||
|
}
|
||||||
|
if options.ReconnectInitialDelay < 0 || options.ReconnectMaxDelay < 0 || options.ReconnectJitter < 0 || options.ReconnectJitter > 100 {
|
||||||
|
return ErrInvalidOptions
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfiguration(ctx context.Context, path string, resolver config.Resolver) (*config.Config, error) {
|
||||||
|
if ctx == nil || strings.TrimSpace(path) != path || path == "" || nilInterface(resolver) {
|
||||||
|
return nil, ErrInvalidOptions
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content, err := resolver.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read configuration %q: %w", path, err)
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
configuration, err := config.LoadResolved(bytes.NewReader(content), resolver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode configuration %q: %w", path, err)
|
||||||
|
}
|
||||||
|
return configuration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type runtime struct {
|
||||||
|
connection *grpc.ClientConn
|
||||||
|
group *lifecycle.Group
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuntime(ctx context.Context, configuration *config.Config, options Options) (*runtime, error) {
|
||||||
|
if ctx == nil || configuration == nil {
|
||||||
|
return nil, ErrInvalidOptions
|
||||||
|
}
|
||||||
|
store := snapshot.NewStore(options.ClusterID, options.WorkerID)
|
||||||
|
protection, err := server.BuildProtection(configuration.Gateway)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build gateway protections: %w", err)
|
||||||
|
}
|
||||||
|
targets, err := server.TargetPolicyFromListener(configuration.Gateway)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build gateway target policy: %w", err)
|
||||||
|
}
|
||||||
|
proxyTransport := transport.New(transport.Config{}, snapshotCredentialResolver{store: store})
|
||||||
|
handler, err := server.New(server.ConfigFromListener(configuration.Gateway), server.Dependencies{
|
||||||
|
Auth: protection.Auth,
|
||||||
|
Access: protection.Access,
|
||||||
|
Admission: protection.Admission,
|
||||||
|
Targets: targets,
|
||||||
|
Router: server.NewSnapshotRouter(store),
|
||||||
|
Dispatcher: dispatch.New(store),
|
||||||
|
Transport: proxyTransport,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
proxyTransport.CloseIdleConnections()
|
||||||
|
return nil, fmt.Errorf("build gateway handler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transportCredentials, err := controlPlaneTransport(configuration.ControlPlane, options.ControlPlaneAddress, options.GRPCTransport)
|
||||||
|
if err != nil {
|
||||||
|
proxyTransport.CloseIdleConnections()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
connection, err := grpc.NewClient(options.ControlPlaneAddress,
|
||||||
|
grpc.WithTransportCredentials(transportCredentials),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.MaxCallRecvMsgSize(configuration.ControlPlane.MaxMessageBytes),
|
||||||
|
grpc.MaxCallSendMsgSize(configuration.ControlPlane.MaxMessageBytes),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
proxyTransport.CloseIdleConnections()
|
||||||
|
return nil, fmt.Errorf("dial control plane: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeConnection := true
|
||||||
|
defer func() {
|
||||||
|
if closeConnection {
|
||||||
|
_ = connection.Close()
|
||||||
|
proxyTransport.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
client := controlplanev1.NewWorkerControlPlaneClient(connection)
|
||||||
|
reporter, err := controlplane.NewRuntimeReporter(generatedRuntimeClient{client: client}, store, controlplane.Options{
|
||||||
|
WorkerID: options.WorkerID, InstanceID: options.InstanceID, Zone: options.Zone,
|
||||||
|
ProtocolVersion: configuration.ControlPlane.ProtocolVersion, Labels: options.Labels, Now: time.Now,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build runtime reporter: %w", err)
|
||||||
|
}
|
||||||
|
watcher, err := controlplane.NewSnapshotWatcher(controlplane.NewGeneratedSnapshotRPCClient(client), store,
|
||||||
|
controlplane.SnapshotWatcherOptions{ClusterID: options.ClusterID, WorkerID: options.WorkerID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build snapshot watcher: %w", err)
|
||||||
|
}
|
||||||
|
session, err := controlplane.NewSessionRunner(reporter, watcher)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build control plane session: %w", err)
|
||||||
|
}
|
||||||
|
reconnect, err := controlplane.NewSessionSupervisor(session, reconnectOptions(options))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build control plane reconnect supervisor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRuntime, err := newHTTPRuntime(ctx, configuration, options, handler, store)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
group, err := lifecycle.NewGroup(httpRuntime, reconnect)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
closeConnection = false
|
||||||
|
return &runtime{connection: connection, group: group}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type generatedRuntimeClient struct {
|
||||||
|
client controlplanev1.WorkerControlPlaneClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type snapshotCredentialResolver struct {
|
||||||
|
store *snapshot.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver snapshotCredentialResolver) Resolve(ctx context.Context, selected proxyDomain.Proxy) (transport.Credentials, error) {
|
||||||
|
credential, err := resolver.store.Credential(ctx, selected)
|
||||||
|
if err != nil {
|
||||||
|
return transport.Credentials{}, err
|
||||||
|
}
|
||||||
|
return transport.Credentials{Username: credential.Username, Password: credential.Password}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client generatedRuntimeClient) RegisterWorker(
|
||||||
|
ctx context.Context,
|
||||||
|
request *controlplanev1.RegisterWorkerRequest,
|
||||||
|
) (*controlplanev1.RegisterWorkerResponse, error) {
|
||||||
|
return client.client.RegisterWorker(ctx, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client generatedRuntimeClient) ReportRuntime(
|
||||||
|
ctx context.Context,
|
||||||
|
request *controlplanev1.ReportRuntimeRequest,
|
||||||
|
) (*controlplanev1.ReportRuntimeResponse, error) {
|
||||||
|
return client.client.ReportRuntime(ctx, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runtime *runtime) Run(ctx context.Context) error {
|
||||||
|
if runtime == nil || runtime.group == nil || ctx == nil {
|
||||||
|
return ErrInvalidOptions
|
||||||
|
}
|
||||||
|
return runtime.group.Run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runtime *runtime) Close() {
|
||||||
|
if runtime != nil && runtime.connection != nil {
|
||||||
|
_ = runtime.connection.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reconnectOptions(options Options) controlplane.ReconnectOptions {
|
||||||
|
initial := options.ReconnectInitialDelay
|
||||||
|
if initial == 0 {
|
||||||
|
initial = defaultReconnectInitialDelay
|
||||||
|
}
|
||||||
|
maximum := options.ReconnectMaxDelay
|
||||||
|
if maximum == 0 {
|
||||||
|
maximum = defaultReconnectMaxDelay
|
||||||
|
}
|
||||||
|
jitter := options.ReconnectJitter
|
||||||
|
if jitter == 0 {
|
||||||
|
jitter = defaultReconnectJitter
|
||||||
|
}
|
||||||
|
return controlplane.ReconnectOptions{InitialDelay: initial, MaxDelay: maximum, Jitter: jitter}
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpRuntime struct {
|
||||||
|
options httpserver.Options
|
||||||
|
handler *server.Handler
|
||||||
|
endpoints []httpserver.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPRuntime(
|
||||||
|
ctx context.Context,
|
||||||
|
configuration *config.Config,
|
||||||
|
options Options,
|
||||||
|
handler *server.Handler,
|
||||||
|
store *snapshot.Store,
|
||||||
|
) (*httpRuntime, error) {
|
||||||
|
if ctx == nil || configuration == nil || handler == nil || store == nil {
|
||||||
|
return nil, ErrInvalidOptions
|
||||||
|
}
|
||||||
|
endpoints := make([]httpserver.Endpoint, 0, 2)
|
||||||
|
gatewayListener, gatewayOwned, err := resolveListener(ctx, configuration.Gateway.Listen, options.GatewayListener)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listen gateway: %w", err)
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, httpserver.Endpoint{Name: "gateway", Listener: gatewayListener, Handler: handler})
|
||||||
|
if configuration.Metrics.Enabled {
|
||||||
|
metricsHandler, metricsErr := platformMetrics.NewHandler(platformMetrics.Dependencies{
|
||||||
|
Gatherer: prometheus.DefaultGatherer,
|
||||||
|
Readiness: snapshotReadiness{store: store, now: time.Now},
|
||||||
|
})
|
||||||
|
if metricsErr != nil {
|
||||||
|
if gatewayOwned {
|
||||||
|
_ = gatewayListener.Close()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("build gateway metrics: %w", metricsErr)
|
||||||
|
}
|
||||||
|
metricsListener, metricsOwned, listenErr := resolveListener(ctx, configuration.Metrics.Listen, options.MetricsListener)
|
||||||
|
if listenErr != nil {
|
||||||
|
if gatewayOwned {
|
||||||
|
_ = gatewayListener.Close()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("listen gateway metrics: %w", listenErr)
|
||||||
|
}
|
||||||
|
_ = metricsOwned
|
||||||
|
endpoints = append(endpoints, httpserver.Endpoint{Name: "metrics", Listener: metricsListener, Handler: metricsHandler})
|
||||||
|
} else if options.MetricsListener != nil {
|
||||||
|
if gatewayOwned {
|
||||||
|
_ = gatewayListener.Close()
|
||||||
|
}
|
||||||
|
return nil, ErrInvalidOptions
|
||||||
|
}
|
||||||
|
return &httpRuntime{options: options.HTTP, handler: handler, endpoints: endpoints}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runtime *httpRuntime) Run(ctx context.Context) error {
|
||||||
|
if runtime == nil || runtime.handler == nil || len(runtime.endpoints) == 0 || ctx == nil {
|
||||||
|
return ErrInvalidOptions
|
||||||
|
}
|
||||||
|
completed := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
shutdownContext, cancel := context.WithTimeout(context.Background(), shutdownTimeout(runtime.options))
|
||||||
|
defer cancel()
|
||||||
|
_ = runtime.handler.Shutdown(shutdownContext)
|
||||||
|
case <-completed:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err := httpserver.Serve(ctx, runtime.options, runtime.endpoints...)
|
||||||
|
close(completed)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveListener(ctx context.Context, address string, listener net.Listener) (net.Listener, bool, error) {
|
||||||
|
if listener != nil {
|
||||||
|
return listener, false, nil
|
||||||
|
}
|
||||||
|
resolved, err := (&net.ListenConfig{}).Listen(ctx, "tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return resolved, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shutdownTimeout(options httpserver.Options) time.Duration {
|
||||||
|
if options.ShutdownTimeout > 0 {
|
||||||
|
return options.ShutdownTimeout
|
||||||
|
}
|
||||||
|
return httpserver.DefaultOptions().ShutdownTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
type snapshotReadiness struct {
|
||||||
|
store *snapshot.Store
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (readiness snapshotReadiness) Ready(ctx context.Context) error {
|
||||||
|
if ctx == nil || readiness.store == nil || readiness.now == nil {
|
||||||
|
return ErrNotReady
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current := readiness.store.Current()
|
||||||
|
if current == nil || current.ValidUntil.IsZero() || !current.ValidUntil.After(readiness.now().UTC()) {
|
||||||
|
return ErrNotReady
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func controlPlaneTransport(
|
||||||
|
configuration config.ControlPlane,
|
||||||
|
address string,
|
||||||
|
override credentials.TransportCredentials,
|
||||||
|
) (credentials.TransportCredentials, error) {
|
||||||
|
if override != nil {
|
||||||
|
return override, nil
|
||||||
|
}
|
||||||
|
switch configuration.TLS.Mode {
|
||||||
|
case "disabled":
|
||||||
|
if !loopbackAddress(address) {
|
||||||
|
return nil, fmt.Errorf("%w: plaintext control plane target must be loopback", ErrInvalidOptions)
|
||||||
|
}
|
||||||
|
return insecure.NewCredentials(), nil
|
||||||
|
case "mtls":
|
||||||
|
if configuration.GatewayTLS.CertFile == "" || configuration.GatewayTLS.KeyFile == "" || configuration.GatewayTLS.ServerCAFile == "" {
|
||||||
|
return nil, fmt.Errorf("%w: controlPlane.gatewayTLS is required for mtls", ErrInvalidOptions)
|
||||||
|
}
|
||||||
|
certificate, err := tls.LoadX509KeyPair(configuration.GatewayTLS.CertFile, configuration.GatewayTLS.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load gateway control plane certificate: %w", err)
|
||||||
|
}
|
||||||
|
caPEM, err := os.ReadFile(configuration.GatewayTLS.ServerCAFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read gateway control plane CA: %w", err)
|
||||||
|
}
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
if !roots.AppendCertsFromPEM(caPEM) {
|
||||||
|
return nil, errors.New("parse gateway control plane CA")
|
||||||
|
}
|
||||||
|
host, _, _ := net.SplitHostPort(address)
|
||||||
|
return credentials.NewTLS(&tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, RootCAs: roots,
|
||||||
|
ServerName: strings.Trim(host, "[]"),
|
||||||
|
}), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: unsupported control plane tls mode", ErrInvalidOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validDialAddress(address string) bool {
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil || host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(port, 10, 16)
|
||||||
|
if err != nil || value == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parsed := net.ParseIP(strings.Trim(host, "[]"))
|
||||||
|
return parsed == nil || !parsed.IsUnspecified()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loopbackAddress(address string) bool {
|
||||||
|
host, _, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host = strings.Trim(host, "[]")
|
||||||
|
if strings.EqualFold(host, "localhost") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
return ip != nil && ip.IsLoopback()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nilInterface(value any) bool {
|
||||||
|
if value == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
reflected := reflect.ValueOf(value)
|
||||||
|
switch reflected.Kind() {
|
||||||
|
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||||
|
return reflected.IsNil()
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
296
internal/gateway/bootstrap/bootstrap_test.go
Normal file
296
internal/gateway/bootstrap/bootstrap_test.go
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
controlplanev1 "proxy-pool/gen/controlplane/v1"
|
||||||
|
"proxy-pool/internal/config"
|
||||||
|
"proxy-pool/internal/controlplane/snapshotwire"
|
||||||
|
"proxy-pool/internal/gateway/snapshot"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunServesGatewayOnlyAfterApplyingControlPlaneSnapshot(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
target := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.URL.Path != "/through-gateway" {
|
||||||
|
t.Fatalf("target path = %q, want /through-gateway", request.URL.Path)
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(writer, "direct-route")
|
||||||
|
}))
|
||||||
|
t.Cleanup(target.Close)
|
||||||
|
|
||||||
|
controlListener := mustListen(t)
|
||||||
|
controlServer := grpc.NewServer()
|
||||||
|
controlplanev1.RegisterWorkerControlPlaneServer(controlServer, &snapshotServer{
|
||||||
|
snapshot: testSnapshot(t, target.URL),
|
||||||
|
})
|
||||||
|
go func() { _ = controlServer.Serve(controlListener) }()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
controlServer.Stop()
|
||||||
|
_ = controlListener.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
proxyListener := mustListen(t)
|
||||||
|
metricsListener := mustListen(t)
|
||||||
|
configPath := writeConfig(t, target.URL)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
result := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
result <- Run(ctx, Options{
|
||||||
|
ConfigPath: configPath,
|
||||||
|
Resolver: config.OSResolver{},
|
||||||
|
ControlPlaneAddress: controlListener.Addr().String(),
|
||||||
|
ClusterID: "cluster-a",
|
||||||
|
WorkerID: "worker-a",
|
||||||
|
InstanceID: "instance-a",
|
||||||
|
Zone: "zone-a",
|
||||||
|
GatewayListener: proxyListener,
|
||||||
|
MetricsListener: metricsListener,
|
||||||
|
GRPCTransport: insecure.NewCredentials(),
|
||||||
|
ReconnectInitialDelay: 5 * time.Millisecond,
|
||||||
|
ReconnectMaxDelay: 20 * time.Millisecond,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := waitForStatus("http://"+metricsListener.Addr().String()+"/readyz", http.StatusOK); err != nil {
|
||||||
|
select {
|
||||||
|
case runErr := <-result:
|
||||||
|
t.Fatalf("gateway did not become ready: %v; Run() error = %v", err, runErr)
|
||||||
|
default:
|
||||||
|
t.Fatalf("gateway did not become ready: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyURL, err := url.Parse("http://" + proxyListener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse(proxy URL): %v", err)
|
||||||
|
}
|
||||||
|
response, err := (&http.Client{Timeout: time.Second, Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxyURL),
|
||||||
|
}}).Get(target.URL + "/through-gateway")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET through gateway: %v", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadAll(gateway response): %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK || string(body) != "direct-route" {
|
||||||
|
t.Fatalf("gateway response = (%d, %q)", response.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case err := <-result:
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Run() did not stop after cancellation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRejectsMissingControlPlaneEndpoint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := Run(context.Background(), Options{
|
||||||
|
ConfigPath: "config.yaml", Resolver: config.OSResolver{},
|
||||||
|
ClusterID: "cluster-a", WorkerID: "worker-a", InstanceID: "instance-a", Zone: "zone-a",
|
||||||
|
})
|
||||||
|
if !errors.Is(err, ErrInvalidOptions) {
|
||||||
|
t.Fatalf("Run() error = %v, want ErrInvalidOptions", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlPlaneTransportRequiresDedicatedGatewayTLS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := controlPlaneTransport(config.ControlPlane{
|
||||||
|
TLS: config.ControlPlaneTLS{Mode: "mtls"},
|
||||||
|
}, "controller.example:8443", nil)
|
||||||
|
if !errors.Is(err, ErrInvalidOptions) {
|
||||||
|
t.Fatalf("controlPlaneTransport() error = %v, want ErrInvalidOptions", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotReadinessRequiresCurrentSnapshot(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := snapshot.NewStore("cluster-a", "worker-a")
|
||||||
|
now := time.Now().UTC()
|
||||||
|
readiness := snapshotReadiness{store: store, now: func() time.Time { return now }}
|
||||||
|
if err := readiness.Ready(context.Background()); !errors.Is(err, ErrNotReady) {
|
||||||
|
t.Fatalf("Ready() before snapshot error = %v, want ErrNotReady", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.Apply(snapshot.Envelope{
|
||||||
|
ClusterID: "cluster-a", WorkerID: "worker-a", Epoch: 1, Version: 1, Full: true,
|
||||||
|
Checksum: snapshot.ChecksumWithRouting(nil, nil), ValidUntil: now.Add(time.Second),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Apply() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := readiness.Ready(context.Background()); err != nil {
|
||||||
|
t.Fatalf("Ready() after snapshot error = %v", err)
|
||||||
|
}
|
||||||
|
now = now.Add(2 * time.Second)
|
||||||
|
if err := readiness.Ready(context.Background()); !errors.Is(err, ErrNotReady) {
|
||||||
|
t.Fatalf("Ready() after expiry error = %v, want ErrNotReady", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type snapshotServer struct {
|
||||||
|
controlplanev1.UnimplementedWorkerControlPlaneServer
|
||||||
|
snapshot *controlplanev1.WorkerSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *snapshotServer) RegisterWorker(_ context.Context, request *controlplanev1.RegisterWorkerRequest) (*controlplanev1.RegisterWorkerResponse, error) {
|
||||||
|
if request.GetWorkerId() != "worker-a" || request.GetInstanceId() != "instance-a" || request.GetZone() != "zone-a" {
|
||||||
|
return nil, errors.New("unexpected worker registration")
|
||||||
|
}
|
||||||
|
return &controlplanev1.RegisterWorkerResponse{
|
||||||
|
WorkerId: request.GetWorkerId(), SessionId: "session-a", OwnershipEpoch: 1,
|
||||||
|
HeartbeatInterval: durationpb.New(time.Hour), MaxStaleAge: durationpb.New(2 * time.Hour),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *snapshotServer) WatchSnapshots(_ *controlplanev1.WatchSnapshotsRequest, stream grpc.ServerStreamingServer[controlplanev1.SnapshotEnvelope]) error {
|
||||||
|
if err := stream.Send(&controlplanev1.SnapshotEnvelope{Payload: &controlplanev1.SnapshotEnvelope_Full{Full: server.snapshot}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
<-stream.Context().Done()
|
||||||
|
return stream.Context().Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *snapshotServer) AcknowledgeSnapshot(context.Context, *controlplanev1.AcknowledgeSnapshotRequest) (*emptypb.Empty, error) {
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *snapshotServer) ReportRuntime(context.Context, *controlplanev1.ReportRuntimeRequest) (*controlplanev1.ReportRuntimeResponse, error) {
|
||||||
|
return &controlplanev1.ReportRuntimeResponse{AcceptedOwnershipEpoch: 1}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSnapshot(t *testing.T, _ string) *controlplanev1.WorkerSnapshot {
|
||||||
|
t.Helper()
|
||||||
|
generated := time.Now().UTC()
|
||||||
|
result := &controlplanev1.WorkerSnapshot{
|
||||||
|
Version: 1, OwnershipEpoch: 1, GeneratedAt: timestamppb.New(generated), ValidUntil: timestamppb.New(generated.Add(time.Minute)),
|
||||||
|
Routing: []*controlplanev1.RoutingRule{{
|
||||||
|
Name: "gateway-default", Enabled: true, HostRegex: ".+", Upstreams: []string{"provider-a"},
|
||||||
|
Strategy: &controlplanev1.RoutingStrategy{Type: controlplanev1.StrategyType_STRATEGY_TYPE_RANDOM},
|
||||||
|
OnUnavailable: controlplanev1.UnavailableAction_UNAVAILABLE_ACTION_DIRECT,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
checksum, err := snapshotwire.Checksum(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Checksum() error = %v", err)
|
||||||
|
}
|
||||||
|
result.Checksum = checksum[:]
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfig(t *testing.T, target string) string {
|
||||||
|
t.Helper()
|
||||||
|
parsed, err := url.Parse(target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse(target URL): %v", err)
|
||||||
|
}
|
||||||
|
port := parsed.Port()
|
||||||
|
if port == "" {
|
||||||
|
t.Fatalf("target URL has no explicit port: %q", target)
|
||||||
|
}
|
||||||
|
path := filepath.Join(t.TempDir(), "gateway.yaml")
|
||||||
|
content := fmt.Sprintf(`version: 1
|
||||||
|
security:
|
||||||
|
requireProtectionOnPublicListen: false
|
||||||
|
gateway:
|
||||||
|
enabled: true
|
||||||
|
listen: 127.0.0.1:0
|
||||||
|
auth: {mode: none}
|
||||||
|
destinationPolicy:
|
||||||
|
denyPrivateNetworks: false
|
||||||
|
denyLoopback: false
|
||||||
|
denyLinkLocal: false
|
||||||
|
allowedPorts: [%s]
|
||||||
|
distribution:
|
||||||
|
enabled: false
|
||||||
|
admin:
|
||||||
|
enabled: false
|
||||||
|
controlPlane:
|
||||||
|
enabled: true
|
||||||
|
listen: 127.0.0.1:0
|
||||||
|
protocolVersion: 1
|
||||||
|
heartbeatInterval: 1s
|
||||||
|
sessionTTL: 3s
|
||||||
|
maxStaleAge: 2s
|
||||||
|
maxMessageBytes: 1048576
|
||||||
|
maxRuntimeCounters: 10
|
||||||
|
maxConcurrentStreams: 10
|
||||||
|
tls: {mode: disabled}
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
listen: 127.0.0.1:0
|
||||||
|
upstreams:
|
||||||
|
provider-a:
|
||||||
|
enabled: true
|
||||||
|
exposure: [gateway]
|
||||||
|
provider: {billingMode: fetch, protocols: [http]}
|
||||||
|
api: {url: https://provider.invalid/api, method: GET, auth: {type: none}}
|
||||||
|
proxyAuth: {type: response}
|
||||||
|
pool: {maxSize: 1}
|
||||||
|
capacity: {maxConcurrencyPerProxy: 2}
|
||||||
|
refill: {reconcileInterval: 1s, minimumAvailableSlots: 1, targetAvailableSlots: 2}
|
||||||
|
lifecycle: {ttl: 1m, allocationSafetyMargin: 1s}
|
||||||
|
fetch: {estimatedIPsPerCall: 1, timeout: 1s, maxAttempts: 1, maxInFlight: 1}
|
||||||
|
`, port)
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile(): %v", err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustListen(t *testing.T) net.Listener {
|
||||||
|
t.Helper()
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Listen(): %v", err)
|
||||||
|
}
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForStatus(target string, want int) error {
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
last := "no response"
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
response, err := (&http.Client{Timeout: 100 * time.Millisecond}).Get(target)
|
||||||
|
if err == nil {
|
||||||
|
_ = response.Body.Close()
|
||||||
|
if response.StatusCode == want {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
last = response.Status
|
||||||
|
} else {
|
||||||
|
last = err.Error()
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("GET %s did not return %d: last result %s", target, want, last)
|
||||||
|
}
|
||||||
@ -126,6 +126,10 @@ func (watcher *SnapshotWatcher) applyFull(full *controlplanev1.WorkerSnapshot) e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
credentials, err := wireSnapshotCredentials(full.GetCredentials(), proxies)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
routes, err := wireRouting(full.GetRouting())
|
routes, err := wireRouting(full.GetRouting())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -133,12 +137,42 @@ func (watcher *SnapshotWatcher) applyFull(full *controlplanev1.WorkerSnapshot) e
|
|||||||
envelope := snapshot.Envelope{
|
envelope := snapshot.Envelope{
|
||||||
ClusterID: watcher.options.ClusterID, WorkerID: watcher.options.WorkerID,
|
ClusterID: watcher.options.ClusterID, WorkerID: watcher.options.WorkerID,
|
||||||
Epoch: full.GetOwnershipEpoch(), Version: full.GetVersion(), Full: true, ValidUntil: validUntil,
|
Epoch: full.GetOwnershipEpoch(), Version: full.GetVersion(), Full: true, ValidUntil: validUntil,
|
||||||
Proxies: proxies, Routing: routes,
|
Proxies: proxies, Routing: routes, Credentials: credentials,
|
||||||
}
|
}
|
||||||
envelope.Checksum = snapshot.ChecksumWithRouting(proxies, routes)
|
envelope.Checksum = snapshot.ChecksumWithCredentials(proxies, routes, credentials)
|
||||||
return watcher.store.Apply(envelope)
|
return watcher.store.Apply(envelope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wireSnapshotCredentials(source []*controlplanev1.SnapshotCredential, proxies []proxyDomain.Proxy) ([]snapshot.Credential, error) {
|
||||||
|
result := make([]snapshot.Credential, len(source))
|
||||||
|
indexed := make(map[string]struct{}, len(source))
|
||||||
|
for index, item := range source {
|
||||||
|
if item == nil || !workerruntime.ValidIdentifier(item.GetSecretRef()) || !workerruntime.ValidIdentifier(item.GetCredentialVersion()) ||
|
||||||
|
(item.GetUsername() == "" && item.GetPassword() == "") {
|
||||||
|
return nil, ErrInvalidSnapshotWatcher
|
||||||
|
}
|
||||||
|
key := item.GetSecretRef() + "\x00" + item.GetCredentialVersion()
|
||||||
|
if _, duplicate := indexed[key]; duplicate {
|
||||||
|
return nil, ErrInvalidSnapshotWatcher
|
||||||
|
}
|
||||||
|
indexed[key] = struct{}{}
|
||||||
|
result[index] = snapshot.Credential{
|
||||||
|
SecretRef: item.GetSecretRef(), CredentialVersion: item.GetCredentialVersion(),
|
||||||
|
Username: item.GetUsername(), Password: item.GetPassword(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, proxy := range proxies {
|
||||||
|
if proxy.SecretRef == "" && proxy.CredentialVersion == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := proxy.SecretRef + "\x00" + proxy.CredentialVersion
|
||||||
|
if _, exists := indexed[key]; !exists {
|
||||||
|
return nil, ErrInvalidSnapshotWatcher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func wireRouting(source []*controlplanev1.RoutingRule) ([]routingDomain.Rule, error) {
|
func wireRouting(source []*controlplanev1.RoutingRule) ([]routingDomain.Rule, error) {
|
||||||
result := make([]routingDomain.Rule, 0, len(source))
|
result := make([]routingDomain.Rule, 0, len(source))
|
||||||
names := make(map[string]struct{}, len(source))
|
names := make(map[string]struct{}, len(source))
|
||||||
|
|||||||
@ -23,7 +23,11 @@ func TestSnapshotWatcherAppliesVerifiedFullSnapshotAndAcknowledges(t *testing.T)
|
|||||||
Version: 1, OwnershipEpoch: 7, GeneratedAt: timestamppb.New(time.Now()), ValidUntil: timestamppb.New(time.Now().Add(time.Minute)),
|
Version: 1, OwnershipEpoch: 7, GeneratedAt: timestamppb.New(time.Now()), ValidUntil: timestamppb.New(time.Now().Add(time.Minute)),
|
||||||
Proxies: []*controlplanev1.OwnedProxy{{
|
Proxies: []*controlplanev1.OwnedProxy{{
|
||||||
Id: "proxy-a", Upstream: "upstream-a", Protocol: controlplanev1.ProxyProtocol_PROXY_PROTOCOL_HTTP,
|
Id: "proxy-a", Upstream: "upstream-a", Protocol: controlplanev1.ProxyProtocol_PROXY_PROTOCOL_HTTP,
|
||||||
Host: "192.0.2.10", Port: 8080, MaxConcurrency: 3, ExpiresAt: timestamppb.New(time.Now().Add(time.Minute)),
|
Host: "192.0.2.10", Port: 8080, Username: "upstream", SecretRef: "cred_a", CredentialVersion: "v1",
|
||||||
|
MaxConcurrency: 3, ExpiresAt: timestamppb.New(time.Now().Add(time.Minute)),
|
||||||
|
}},
|
||||||
|
Credentials: []*controlplanev1.SnapshotCredential{{
|
||||||
|
SecretRef: "cred_a", CredentialVersion: "v1", Username: "upstream", Password: "secret",
|
||||||
}},
|
}},
|
||||||
Routing: []*controlplanev1.RoutingRule{{
|
Routing: []*controlplanev1.RoutingRule{{
|
||||||
Name: "gateway-api", Enabled: true, HostRegex: `^api\.example\.test$`, Upstreams: []string{"upstream-a"},
|
Name: "gateway-api", Enabled: true, HostRegex: `^api\.example\.test$`, Upstreams: []string{"upstream-a"},
|
||||||
@ -54,6 +58,10 @@ func TestSnapshotWatcherAppliesVerifiedFullSnapshotAndAcknowledges(t *testing.T)
|
|||||||
if client.watch.GetSessionId() != "session-a" || client.ack.GetVersion() != 1 || !client.ack.GetApplied() || string(client.ack.GetChecksum()) != string(full.GetChecksum()) {
|
if client.watch.GetSessionId() != "session-a" || client.ack.GetVersion() != 1 || !client.ack.GetApplied() || string(client.ack.GetChecksum()) != string(full.GetChecksum()) {
|
||||||
t.Fatalf("watch=%+v ack=%+v", client.watch, client.ack)
|
t.Fatalf("watch=%+v ack=%+v", client.watch, client.ack)
|
||||||
}
|
}
|
||||||
|
credential, err := store.Credential(context.Background(), view.Entries[0].Proxy)
|
||||||
|
if err != nil || credential.Username != "upstream" || credential.Password != "secret" {
|
||||||
|
t.Fatalf("Credential() = (%+v, %v)", credential, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSnapshotWatcherRejectsChecksumAndAcknowledgesFailure(t *testing.T) {
|
func TestSnapshotWatcherRejectsChecksumAndAcknowledgesFailure(t *testing.T) {
|
||||||
|
|||||||
@ -277,7 +277,6 @@ func proxyDescriptor(t *testing.T, id, rawURL string) proxyDomain.Proxy {
|
|||||||
SourceUpstream: "provider-a",
|
SourceUpstream: "provider-a",
|
||||||
MaxConcurrency: 2,
|
MaxConcurrency: 2,
|
||||||
State: proxyDomain.StateAvailable,
|
State: proxyDomain.StateAvailable,
|
||||||
CredentialVersion: "v1",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -595,7 +595,6 @@ func dispatcherWithProxies(t *testing.T, ids ...string) (*dispatch.Dispatcher, *
|
|||||||
SourceUpstream: "provider-a",
|
SourceUpstream: "provider-a",
|
||||||
MaxConcurrency: 2,
|
MaxConcurrency: 2,
|
||||||
State: proxyDomain.StateAvailable,
|
State: proxyDomain.StateAvailable,
|
||||||
CredentialVersion: "v1",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
envelope := snapshot.Envelope{
|
envelope := snapshot.Envelope{
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package snapshot
|
package snapshot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -24,6 +25,8 @@ var (
|
|||||||
ErrInvalidRuntimeReport = errors.New("invalid worker runtime report")
|
ErrInvalidRuntimeReport = errors.New("invalid worker runtime report")
|
||||||
ErrInvalidRuntimeLimit = errors.New("invalid snapshot runtime limit")
|
ErrInvalidRuntimeLimit = errors.New("invalid snapshot runtime limit")
|
||||||
ErrRuntimeLimitExceeded = errors.New("snapshot runtime limit exceeded")
|
ErrRuntimeLimitExceeded = errors.New("snapshot runtime limit exceeded")
|
||||||
|
ErrInvalidCredential = errors.New("invalid snapshot credential")
|
||||||
|
ErrCredentialMissing = errors.New("snapshot credential is missing")
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultRuntimeLimit = 1_000_000
|
const defaultRuntimeLimit = 1_000_000
|
||||||
@ -102,6 +105,20 @@ type Envelope struct {
|
|||||||
ValidUntil time.Time
|
ValidUntil time.Time
|
||||||
Proxies []proxyDomain.Proxy
|
Proxies []proxyDomain.Proxy
|
||||||
Routing []routing.Rule
|
Routing []routing.Rule
|
||||||
|
Credentials []Credential
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credential is ephemeral material received through a verified mTLS snapshot.
|
||||||
|
// Its Format method prevents accidental diagnostics from exposing either value.
|
||||||
|
type Credential struct {
|
||||||
|
SecretRef string
|
||||||
|
CredentialVersion string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Credential) Format(state fmt.State, _ rune) {
|
||||||
|
_, _ = state.Write([]byte("snapshot.Credential{SecretRef:<redacted>, CredentialVersion:<redacted>, Username:<redacted>, Password:<redacted>}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
@ -118,6 +135,7 @@ type View struct {
|
|||||||
ValidUntil time.Time
|
ValidUntil time.Time
|
||||||
Entries []Entry
|
Entries []Entry
|
||||||
routing *routing.RuleSet
|
routing *routing.RuleSet
|
||||||
|
credentials map[string]Credential
|
||||||
|
|
||||||
all []int
|
all []int
|
||||||
byScheme map[proxyDomain.Scheme][]int
|
byScheme map[proxyDomain.Scheme][]int
|
||||||
@ -229,13 +247,17 @@ func (s *Store) Apply(envelope Envelope) error {
|
|||||||
if !envelope.Full || envelope.Epoch == 0 || envelope.Version == 0 {
|
if !envelope.Full || envelope.Epoch == 0 || envelope.Version == 0 {
|
||||||
return ErrResyncRequired
|
return ErrResyncRequired
|
||||||
}
|
}
|
||||||
if envelope.Checksum != ChecksumWithRouting(envelope.Proxies, envelope.Routing) {
|
if envelope.Checksum != ChecksumWithCredentials(envelope.Proxies, envelope.Routing, envelope.Credentials) {
|
||||||
return ErrChecksumMismatch
|
return ErrChecksumMismatch
|
||||||
}
|
}
|
||||||
routes, err := routing.Compile(envelope.Routing)
|
routes, err := routing.Compile(envelope.Routing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("compile snapshot routing: %w", err)
|
return fmt.Errorf("compile snapshot routing: %w", err)
|
||||||
}
|
}
|
||||||
|
credentials, err := indexCredentials(envelope.Credentials, envelope.Proxies)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@ -323,12 +345,48 @@ func (s *Store) Apply(envelope Envelope) error {
|
|||||||
ValidUntil: envelope.ValidUntil.UTC(),
|
ValidUntil: envelope.ValidUntil.UTC(),
|
||||||
Entries: entries,
|
Entries: entries,
|
||||||
routing: routes,
|
routing: routes,
|
||||||
|
credentials: credentials,
|
||||||
}
|
}
|
||||||
next.buildIndexes()
|
next.buildIndexes()
|
||||||
s.current.Store(next)
|
s.current.Store(next)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Credential resolves one exact Proxy credential reference from the current
|
||||||
|
// immutable view. Credentialless proxies preserve their metadata username.
|
||||||
|
func (s *Store) Credential(ctx context.Context, selected proxyDomain.Proxy) (Credential, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return Credential{}, context.Canceled
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Credential{}, err
|
||||||
|
}
|
||||||
|
if (selected.SecretRef == "") != (selected.CredentialVersion == "") {
|
||||||
|
return Credential{}, ErrInvalidCredential
|
||||||
|
}
|
||||||
|
if selected.SecretRef == "" {
|
||||||
|
return Credential{Username: selected.Username}, nil
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
return Credential{}, ErrCredentialMissing
|
||||||
|
}
|
||||||
|
current := s.current.Load()
|
||||||
|
if current == nil {
|
||||||
|
return Credential{}, ErrCredentialMissing
|
||||||
|
}
|
||||||
|
credential, ok := current.credentials[credentialKey(selected.SecretRef, selected.CredentialVersion)]
|
||||||
|
if !ok {
|
||||||
|
return Credential{}, ErrCredentialMissing
|
||||||
|
}
|
||||||
|
if selected.Username != "" && credential.Username != "" && selected.Username != credential.Username {
|
||||||
|
return Credential{}, ErrInvalidCredential
|
||||||
|
}
|
||||||
|
if credential.Username == "" {
|
||||||
|
credential.Username = selected.Username
|
||||||
|
}
|
||||||
|
return credential, nil
|
||||||
|
}
|
||||||
|
|
||||||
// MatchRouting matches a request against the immutable rules published with
|
// MatchRouting matches a request against the immutable rules published with
|
||||||
// this proxy view. A View is the atomic consistency boundary for both sets.
|
// this proxy view. A View is the atomic consistency boundary for both sets.
|
||||||
func (v *View) MatchRouting(request routing.Request) (routing.Rule, bool) {
|
func (v *View) MatchRouting(request routing.Request) (routing.Rule, bool) {
|
||||||
@ -427,12 +485,18 @@ func Checksum(proxies []proxyDomain.Proxy) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ChecksumWithRouting(proxies []proxyDomain.Proxy, rules []routing.Rule) string {
|
func ChecksumWithRouting(proxies []proxyDomain.Proxy, rules []routing.Rule) string {
|
||||||
|
return ChecksumWithCredentials(proxies, rules, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChecksumWithCredentials(proxies []proxyDomain.Proxy, rules []routing.Rule, credentials []Credential) string {
|
||||||
payload := struct {
|
payload := struct {
|
||||||
Proxies []proxyDomain.Proxy `json:"proxies"`
|
Proxies []proxyDomain.Proxy `json:"proxies"`
|
||||||
Routing []routing.Rule `json:"routing"`
|
Routing []routing.Rule `json:"routing"`
|
||||||
|
Credentials []Credential `json:"credentials"`
|
||||||
}{
|
}{
|
||||||
Proxies: cloneAndSort(proxies),
|
Proxies: cloneAndSort(proxies),
|
||||||
Routing: cloneRoutingRules(rules),
|
Routing: cloneRoutingRules(rules),
|
||||||
|
Credentials: cloneAndSortCredentials(credentials),
|
||||||
}
|
}
|
||||||
encoded, err := json.Marshal(payload)
|
encoded, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -442,6 +506,46 @@ func ChecksumWithRouting(proxies []proxyDomain.Proxy, rules []routing.Rule) stri
|
|||||||
return hex.EncodeToString(digest[:])
|
return hex.EncodeToString(digest[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func indexCredentials(source []Credential, proxies []proxyDomain.Proxy) (map[string]Credential, error) {
|
||||||
|
indexed := make(map[string]Credential, len(source))
|
||||||
|
for _, credential := range source {
|
||||||
|
if credential.SecretRef == "" || credential.CredentialVersion == "" || (credential.Username == "" && credential.Password == "") {
|
||||||
|
return nil, ErrInvalidCredential
|
||||||
|
}
|
||||||
|
key := credentialKey(credential.SecretRef, credential.CredentialVersion)
|
||||||
|
if _, duplicate := indexed[key]; duplicate {
|
||||||
|
return nil, ErrInvalidCredential
|
||||||
|
}
|
||||||
|
indexed[key] = credential
|
||||||
|
}
|
||||||
|
for _, proxy := range proxies {
|
||||||
|
if (proxy.SecretRef == "") != (proxy.CredentialVersion == "") {
|
||||||
|
return nil, ErrInvalidCredential
|
||||||
|
}
|
||||||
|
if proxy.SecretRef == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
credential, exists := indexed[credentialKey(proxy.SecretRef, proxy.CredentialVersion)]
|
||||||
|
if !exists || (proxy.Username != "" && credential.Username != "" && proxy.Username != credential.Username) {
|
||||||
|
return nil, ErrInvalidCredential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indexed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func credentialKey(secretRef, version string) string { return secretRef + "\x00" + version }
|
||||||
|
|
||||||
|
func cloneAndSortCredentials(source []Credential) []Credential {
|
||||||
|
cloned := append([]Credential(nil), source...)
|
||||||
|
sort.Slice(cloned, func(left, right int) bool {
|
||||||
|
if cloned[left].SecretRef == cloned[right].SecretRef {
|
||||||
|
return cloned[left].CredentialVersion < cloned[right].CredentialVersion
|
||||||
|
}
|
||||||
|
return cloned[left].SecretRef < cloned[right].SecretRef
|
||||||
|
})
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
func cloneRoutingRules(source []routing.Rule) []routing.Rule {
|
func cloneRoutingRules(source []routing.Rule) []routing.Rule {
|
||||||
result := make([]routing.Rule, len(source))
|
result := make([]routing.Rule, len(source))
|
||||||
for index, rule := range source {
|
for index, rule := range source {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package snapshot
|
package snapshot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -126,6 +128,35 @@ func TestStorePublishesRoutingWithTheSameSnapshotVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStoreResolvesCredentialOnlyFromCurrentSnapshot(t *testing.T) {
|
||||||
|
store := NewStore("cluster-a", "worker-a")
|
||||||
|
proxy := proxyDomain.Proxy{
|
||||||
|
ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080, MaxConcurrency: 1,
|
||||||
|
SecretRef: "cred_a", CredentialVersion: "v1", Username: "upstream",
|
||||||
|
}
|
||||||
|
envelope := Envelope{
|
||||||
|
ClusterID: "cluster-a", WorkerID: "worker-a", Epoch: 1, Version: 1, Full: true, Proxies: []proxyDomain.Proxy{proxy},
|
||||||
|
Credentials: []Credential{{SecretRef: "cred_a", CredentialVersion: "v1", Username: "upstream", Password: "secret"}},
|
||||||
|
}
|
||||||
|
envelope.Checksum = ChecksumWithCredentials(envelope.Proxies, envelope.Routing, envelope.Credentials)
|
||||||
|
if err := store.Apply(envelope); err != nil {
|
||||||
|
t.Fatalf("Apply(): %v", err)
|
||||||
|
}
|
||||||
|
credential, err := store.Credential(context.Background(), proxy)
|
||||||
|
if err != nil || credential.Username != "upstream" || credential.Password != "secret" {
|
||||||
|
t.Fatalf("Credential() = (%+v, %v)", credential, err)
|
||||||
|
}
|
||||||
|
if formatted := fmt.Sprintf("%+v", credential); strings.Contains(formatted, "secret") || strings.Contains(formatted, "upstream") {
|
||||||
|
t.Fatalf("formatted credential exposes material: %s", formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := Envelope{ClusterID: "cluster-a", WorkerID: "worker-a", Epoch: 1, Version: 2, Full: true, Proxies: []proxyDomain.Proxy{proxy}}
|
||||||
|
missing.Checksum = ChecksumWithCredentials(missing.Proxies, nil, nil)
|
||||||
|
if err := store.Apply(missing); !errors.Is(err, ErrInvalidCredential) {
|
||||||
|
t.Fatalf("Apply(missing credential) error = %v, want ErrInvalidCredential", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestViewSelectFiltersBySchemeUpstreamTagAndExclude(t *testing.T) {
|
func TestViewSelectFiltersBySchemeUpstreamTagAndExclude(t *testing.T) {
|
||||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
||||||
expiresSoon := now.Add(5 * time.Second)
|
expiresSoon := now.Add(5 * time.Second)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user