feat: add grpc runtime transport
This commit is contained in:
44
.github/workflows/ci.yml
vendored
Normal file
44
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: "25.1"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Verify generated protobuf code
|
||||
run: make proto-check
|
||||
|
||||
- name: Go tests
|
||||
run: GOCACHE=/tmp/configcenter-go-cache go test ./...
|
||||
|
||||
- name: Go race tests
|
||||
run: GOCACHE=/tmp/configcenter-go-cache go test -race ./...
|
||||
|
||||
- name: Python SDK compile check
|
||||
run: PYTHONPATH="$PWD/.tools/python:$PWD/sdk/python" python -m compileall -q sdk/python/configcenter
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
.env
|
||||
.idea/
|
||||
.vscode/
|
||||
.tools/
|
||||
bin/
|
||||
coverage.out
|
||||
web/dist/
|
||||
|
||||
36
Makefile
36
Makefile
@@ -1,4 +1,12 @@
|
||||
.PHONY: dev test build web-install web-build compose-up compose-down integration-smoke loadtest
|
||||
.PHONY: dev test build proto proto-go proto-python proto-tools proto-check web-install web-build compose-up compose-down integration-smoke loadtest
|
||||
|
||||
PROTOC_GEN_GO_VERSION := v1.36.5
|
||||
PROTOC_GEN_GO_GRPC_VERSION := v1.5.1
|
||||
GRPCIO_TOOLS_VERSION := 1.71.0
|
||||
PROTO_FILE := api/proto/configcenter/v1/config.proto
|
||||
PROTO_GO_FILES := pkg/proto/v1/config.pb.go pkg/proto/v1/config_grpc.pb.go
|
||||
PROTO_PY_FILES := sdk/python/configcenter/v1/config_pb2.py sdk/python/configcenter/v1/config_pb2_grpc.py
|
||||
PROTO_TOOLS_STAMP := .tools/.proto-tools-$(PROTOC_GEN_GO_VERSION)-$(PROTOC_GEN_GO_GRPC_VERSION)-$(GRPCIO_TOOLS_VERSION)
|
||||
|
||||
dev:
|
||||
go run ./cmd/server
|
||||
@@ -10,6 +18,32 @@ build:
|
||||
mkdir -p bin
|
||||
GOCACHE=/tmp/configcenter-go-cache go build -buildvcs=false -trimpath -o bin/configcenter ./cmd/server
|
||||
|
||||
proto-tools: $(PROTO_TOOLS_STAMP)
|
||||
|
||||
$(PROTO_TOOLS_STAMP):
|
||||
rm -rf .tools/bin .tools/python
|
||||
mkdir -p .tools/bin .tools/python
|
||||
GOBIN=$(CURDIR)/.tools/bin go install google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GEN_GO_VERSION)
|
||||
GOBIN=$(CURDIR)/.tools/bin go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(PROTOC_GEN_GO_GRPC_VERSION)
|
||||
python3 -m pip install --disable-pip-version-check --target .tools/python grpcio-tools==$(GRPCIO_TOOLS_VERSION)
|
||||
touch $(PROTO_TOOLS_STAMP)
|
||||
|
||||
proto-go: proto-tools
|
||||
PATH="$(CURDIR)/.tools/bin:$$PATH" protoc -I api/proto \
|
||||
--go_out=. --go_opt=module=github.com/longpeng/configcenter \
|
||||
--go-grpc_out=. --go-grpc_opt=module=github.com/longpeng/configcenter \
|
||||
$(PROTO_FILE)
|
||||
|
||||
proto-python: proto-tools
|
||||
PYTHONPATH="$(CURDIR)/.tools/python" python3 -m grpc_tools.protoc -I api/proto \
|
||||
--python_out=sdk/python --grpc_python_out=sdk/python $(PROTO_FILE)
|
||||
touch sdk/python/configcenter/v1/__init__.py
|
||||
|
||||
proto: proto-go proto-python
|
||||
|
||||
proto-check: proto
|
||||
git diff --exit-code -- $(PROTO_FILE) $(PROTO_GO_FILES) $(PROTO_PY_FILES) sdk/python/configcenter/v1/__init__.py
|
||||
|
||||
web-install:
|
||||
npm --prefix web install
|
||||
|
||||
|
||||
49
README.md
49
README.md
@@ -9,12 +9,12 @@
|
||||
- PostgreSQL 事务化发布,基于 advisory lock 的并发版本分配;
|
||||
- Outbox claim、租约恢复、指数退避、最大重试和 release 状态回写;
|
||||
- etcd 完整快照 + `__meta` 同事务写入;
|
||||
- 运行时配置读取与 SSE Watch,服务端同 key Watch 扇出;
|
||||
- 运行时配置读取与 SSE/gRPC Watch,统一使用 etcd MVCC revision,支持断线续传与 compact 后全量恢复;
|
||||
- 发布历史和“生成新版本”的可追溯回滚;
|
||||
- 审计日志查询;
|
||||
- 可选 JWT 登录、bcrypt 密码、全局管理员与应用级 `viewer` / `app-owner` RBAC;
|
||||
- IP/CIDR、实例 ID、稳定百分比三类灰度规则,支持优先级覆盖;
|
||||
- Go/Python SDK,支持自动重连、全量替换和本地文件缓存兜底;
|
||||
- Go/Python SDK,支持 REST/SSE 与 gRPC transport、自动重连、全量替换和本地文件缓存兜底;
|
||||
- React Web Console(含登录、灰度、用户授权页面)、Docker Compose 与本地内存开发模式;
|
||||
- Prometheus 指标与告警规则、三副本服务/三节点 etcd Kubernetes 清单、SLO 压测工具。
|
||||
- HTTP 延迟直方图/p95 告警、带校验和的数据库 migration 追踪、etcd mTLS 与定时备份维护任务。
|
||||
@@ -34,7 +34,7 @@ make web-install
|
||||
npm --prefix web run dev
|
||||
```
|
||||
|
||||
浏览器访问 `http://localhost:5173`。API 默认监听 `http://localhost:8080`。
|
||||
浏览器访问 `http://localhost:5173`。HTTP API 默认监听 `http://localhost:8080`,gRPC 默认监听 `localhost:9091`。
|
||||
|
||||
完整依赖环境:
|
||||
|
||||
@@ -55,6 +55,9 @@ docker compose --profile monitoring up --build
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---|---:|---|
|
||||
| `HTTP_ADDR` | `:8080` | HTTP 监听地址 |
|
||||
| `GRPC_ADDR` | `:9091` | gRPC 监听地址 |
|
||||
| `GRPC_TLS_CERT_FILE` | 空 | gRPC 服务端 TLS 证书;与私钥同时配置 |
|
||||
| `GRPC_TLS_KEY_FILE` | 空 | gRPC 服务端 TLS 私钥;与证书同时配置 |
|
||||
| `DATABASE_URL` | 空 | 为空时使用内存控制面 |
|
||||
| `ETCD_ENDPOINTS` | 空 | 逗号分隔;为空时使用内存运行时 |
|
||||
| `ETCD_DIAL_TIMEOUT` | `5s` | etcd 连接超时 |
|
||||
@@ -104,6 +107,8 @@ docker compose --profile monitoring up --build
|
||||
|
||||
认证关闭时服务以开发管理员身份运行,并可用 `X-User` 记录操作者;认证开启时操作者来自 JWT,客户端不能通过请求头伪造。`viewer` 可以读取应用配置,`app-owner` 还可以维护命名空间、配置、发布、回滚和灰度规则,全局管理员可管理应用、环境、审计和用户授权。
|
||||
|
||||
正式 gRPC 契约位于 `api/proto/configcenter/v1/config.proto`,提供 `ConfigService.GetConfig/WatchConfig` 与 `AdminService.PublishConfig/RollbackConfig`。认证令牌通过 `authorization: Bearer <token>` metadata 传递。`WatchConfig.start_revision` 为包含式游标:首次订阅传 `0`,重连传 `last_seen_revision + 1`;若历史已被 compact,服务端发送最新 `FULL_SYNC` 后从快照 revision 的下一版本继续监听。详细一致性约束见 `docs/adr/0001-runtime-revision-watch.md`。
|
||||
|
||||
### 灰度匹配
|
||||
|
||||
`ip` 规则接受精确 IP 或 CIDR;`instance` 规则接受实例 ID 列表;`percentage` 使用 salt 与实例 ID(无实例时使用 IP)做稳定哈希,同一实例不会随机漂移。多条规则命中时按优先级从低到高覆盖,并在响应的 `grayRuleIds` 中返回命中规则。
|
||||
@@ -139,6 +144,24 @@ go client.WatchAndSync(ctx, "application")
|
||||
port := client.GetInt("application", "server.port", 8080)
|
||||
```
|
||||
|
||||
使用 gRPC transport:
|
||||
|
||||
```go
|
||||
client, err := configsdk.NewGRPC(configsdk.GRPCOptions{
|
||||
Target: "configcenter.example.com:9091",
|
||||
Env: "PROD",
|
||||
App: "order-service",
|
||||
Token: os.Getenv("CONFIGCENTER_TOKEN"),
|
||||
Instance: "order-3",
|
||||
CacheFile: "/var/lib/my-service/configcenter.json",
|
||||
})
|
||||
if err != nil { /* handle */ }
|
||||
defer client.Close()
|
||||
|
||||
_ = client.Load(ctx, "application")
|
||||
go client.WatchAndSync(ctx, "application")
|
||||
```
|
||||
|
||||
Python SDK 位于 `sdk/python`:
|
||||
|
||||
```python
|
||||
@@ -153,12 +176,30 @@ client.start_background_watch("application")
|
||||
timeout = client.get("application", "order.timeout.minutes", "30")
|
||||
```
|
||||
|
||||
Python gRPC transport 需要安装可选依赖 `configcenter-client[grpc]`:
|
||||
|
||||
```python
|
||||
client = ConfigClient(
|
||||
"configcenter.example.com:9091",
|
||||
"PROD",
|
||||
"order-service",
|
||||
"/tmp/order-config.json",
|
||||
token=os.environ.get("CONFIGCENTER_TOKEN"),
|
||||
instance="order-3",
|
||||
transport="grpc",
|
||||
)
|
||||
client.load("application")
|
||||
client.start_background_watch("application")
|
||||
```
|
||||
|
||||
本地缓存采用完整快照和原子 rename;Config Server/etcd 暂时不可用时,进程可以读取上一次成功同步的缓存启动。
|
||||
|
||||
## 开发与验证
|
||||
|
||||
```bash
|
||||
make test
|
||||
make proto-check
|
||||
go test -race ./...
|
||||
make build
|
||||
make web-build
|
||||
docker compose config
|
||||
@@ -204,6 +245,7 @@ etcd 服务端证书需覆盖 `etcd`、`etcd.configcenter.svc.cluster.local` 和
|
||||
```text
|
||||
cmd/server 服务入口
|
||||
internal/api/httpapi REST + SSE
|
||||
internal/api/grpcapi gRPC 服务、认证拦截器与错误映射
|
||||
internal/store/postgres PostgreSQL repository 与 migration
|
||||
internal/store/memory 本地开发/测试实现
|
||||
internal/runtime/etcd etcd 快照与 Watch
|
||||
@@ -215,6 +257,7 @@ internal/metrics Prometheus 指标
|
||||
cmd/loadtest 运行时读取 SLO 压测
|
||||
deploy Kubernetes 与监控告警配置
|
||||
pkg/sdk/go Go SDK
|
||||
pkg/proto 生成的 Go protobuf/gRPC 代码
|
||||
sdk/python Python SDK
|
||||
api/proto gRPC 契约
|
||||
web React Web Console
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
|
||||
当前基线已经完成 REST/SSE 管理面与运行时分发、PostgreSQL Outbox、etcd、JWT/RBAC、灰度规则、Web Console、Go/Python SDK、监控告警、压测和 Kubernetes 高可用清单。本文件仅记录尚未完成或必须在真实生产环境验收的工作。
|
||||
|
||||
## P1:实现正式 gRPC 服务
|
||||
## P1(功能阻断):实现正式 gRPC 服务
|
||||
|
||||
- [ ] 为 `api/proto/configcenter/v1/config.proto` 增加可重复执行的 Go/Python 代码生成流程,并在 CI 中检查生成代码未漂移。
|
||||
- [ ] 实现 `ConfigService.GetConfig` 与 `ConfigService.WatchConfig`,复用现有 Store、WatchHub、灰度匹配和 RBAC 逻辑。
|
||||
- [ ] 正确实现 `start_revision`:重连时不丢事件;revision 已 compact 时返回最新完整快照并继续监听。
|
||||
- [ ] 实现 `AdminService.PublishConfig` 与 `AdminService.RollbackConfig`,保持与 REST API 相同的 Outbox、审计和权限语义。
|
||||
- [ ] 增加 JWT metadata unary/stream interceptor、gRPC TLS、健康检查、优雅停机、错误码映射和请求指标。
|
||||
- [ ] 增加 bufconn 集成测试,覆盖读取、发布、回滚、流式断线重连、无权限和过期令牌。
|
||||
- [ ] Go/Python SDK 增加 gRPC transport;REST/SSE transport 在迁移期继续兼容。
|
||||
- [x] 固定 Runtime revision / Watch / Outbox 一致性模型:统一使用 etcd MVCC revision;普通断线从未消费 revision 继续;仅 compact 时回退到 `FULL_SYNC`,见 `docs/adr/0001-runtime-revision-watch.md`。
|
||||
- [x] 为 `api/proto/configcenter/v1/config.proto` 增加可重复执行的 Go/Python 代码生成流程,并在 CI 中检查生成代码未漂移。
|
||||
- [x] 实现 `ConfigService.GetConfig` 与 `ConfigService.WatchConfig`,复用现有 Store、WatchHub、灰度匹配和 RBAC 逻辑。
|
||||
- [x] 在 gRPC 层落实 `start_revision` 契约:`0` 表示首次订阅;`>0` 表示第一个期望接收的 revision;SDK 使用 `last_seen_revision + 1` 重连;revision 已 compact 时返回最新完整快照并从 `snapshot_revision + 1` 继续监听。
|
||||
- [x] 实现 `AdminService.PublishConfig` 与 `AdminService.RollbackConfig`,保持与 REST API 相同的 Outbox、审计和权限语义。
|
||||
- [x] 增加 JWT metadata unary/stream interceptor、gRPC TLS、健康检查、优雅停机、错误码映射和请求指标。
|
||||
- [x] 增加 bufconn 集成测试,覆盖读取、指定 revision 回放、compact 全量恢复、客户端主动断线后重新建流、发布、回滚、无权限和过期令牌。
|
||||
- [x] Go/Python SDK 增加 gRPC transport;REST/SSE transport 在迁移期继续兼容。
|
||||
|
||||
完成标准:gRPC 与 REST 对同一配置范围返回一致结果;断线恢复测试无丢失;认证、竞态检查、静态检查和端到端测试全部通过。
|
||||
|
||||
## P1:生产 mTLS 与灾备实地验收
|
||||
## P1(上线阻断):生产 mTLS 与灾备实地验收
|
||||
|
||||
- [ ] 签发 etcd server、peer、client 证书,覆盖 `etcd`、`etcd.configcenter.svc.cluster.local` 和 `*.etcd-peer.configcenter.svc.cluster.local`。
|
||||
- [ ] 在预生产三节点集群验证双向 TLS、证书轮换、NetworkPolicy、节点滚动重启和 quorum 故障恢复。
|
||||
@@ -32,6 +33,7 @@
|
||||
- [ ] Web Console 增加审计页面、筛选条件、详情查看和分页。
|
||||
- [ ] 增加用户启用/禁用、密码重置和删除接口及管理页面。
|
||||
- [ ] 防止删除或禁用最后一个管理员,并限制用户破坏自己的当前管理会话。
|
||||
- [ ] 明确 JWT 即时失效策略:用户禁用、密码重置或权限收回后,已签发令牌必须在定义的时限内失效,并补齐对应测试。
|
||||
- [ ] 用户状态、密码重置和角色变更全部写入审计日志;补齐 PostgreSQL、内存 Store 和 HTTP 权限测试。
|
||||
|
||||
完成标准:管理员可完整管理用户生命周期并追溯操作;越权、最后管理员保护和令牌失效场景都有自动化测试。
|
||||
@@ -51,9 +53,13 @@
|
||||
- [ ] 建立版本号、变更日志、数据库 migration 发布规则及回滚准入检查。
|
||||
- [ ] 在预生产执行持续压测和故障注入,验证 p95、错误率、Outbox 积压和 Watch 重连告警。
|
||||
|
||||
完成标准:主分支 CI 全部通过;发布产物可追溯、可验证、可回滚;预生产持续压测和故障注入达到既定 SLO;发布前无未处理的高危依赖或镜像漏洞。
|
||||
|
||||
## 推荐实施顺序
|
||||
|
||||
1. gRPC 服务及 SDK transport。
|
||||
2. 审计控制台与用户生命周期。
|
||||
3. Redis ADR 和清理/实现。
|
||||
4. 生产证书、备份恢复及发布工程化验收。
|
||||
1. ~~固定 revision / Watch / Outbox 一致性模型。~~ 已完成。
|
||||
2. ~~gRPC 代码生成、服务端和 SDK transport。~~ 已完成。
|
||||
3. 审计控制台与用户生命周期,并明确 JWT 即时失效策略。
|
||||
4. Redis ADR 和清理/实现。
|
||||
5. CI/CD、制品签名、SBOM、漏洞扫描和发布规则。
|
||||
6. 预生产 mTLS、备份恢复、故障演练与生产准入验收。
|
||||
|
||||
@@ -13,6 +13,8 @@ message GetConfigRequest {
|
||||
string env = 1;
|
||||
string app = 2;
|
||||
string namespace = 3;
|
||||
string ip = 4;
|
||||
string instance = 5;
|
||||
}
|
||||
|
||||
message GetConfigResponse {
|
||||
@@ -26,6 +28,8 @@ message WatchConfigRequest {
|
||||
string app = 2;
|
||||
string namespace = 3;
|
||||
int64 start_revision = 4;
|
||||
string ip = 5;
|
||||
string instance = 6;
|
||||
}
|
||||
|
||||
message ConfigEvent {
|
||||
|
||||
@@ -2,14 +2,23 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/health"
|
||||
grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||
|
||||
"github.com/longpeng/configcenter/internal/api/grpcapi"
|
||||
"github.com/longpeng/configcenter/internal/api/httpapi"
|
||||
"github.com/longpeng/configcenter/internal/auth"
|
||||
"github.com/longpeng/configcenter/internal/config"
|
||||
@@ -66,34 +75,84 @@ func main() {
|
||||
go worker.Run(ctx)
|
||||
|
||||
api := httpapi.New(repository, runtimeStore, hub, authorizer, collector, cfg.AllowedOrigins, logger)
|
||||
server := &http.Server{
|
||||
httpServer := &http.Server{
|
||||
Addr: cfg.HTTPAddr,
|
||||
Handler: api.Handler(),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
grpcAPI := grpcapi.New(repository, runtimeStore, hub, authorizer, collector, logger)
|
||||
grpcOptions, err := grpcOptions(cfg, grpcAPI)
|
||||
if err != nil {
|
||||
logger.Error("configure grpc server", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
grpcServer := grpc.NewServer(grpcOptions...)
|
||||
grpcAPI.Register(grpcServer)
|
||||
healthServer := health.NewServer()
|
||||
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
||||
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||
grpcListener, err := net.Listen("tcp", cfg.GRPCAddr)
|
||||
if err != nil {
|
||||
logger.Error("listen grpc", "address", cfg.GRPCAddr, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
serverErrors := make(chan error, 1)
|
||||
serverErrors := make(chan error, 2)
|
||||
go func() {
|
||||
logger.Info("config center listening", "address", cfg.HTTPAddr)
|
||||
serverErrors <- server.ListenAndServe()
|
||||
logger.Info("config center http listening", "address", cfg.HTTPAddr)
|
||||
serverErrors <- httpServer.ListenAndServe()
|
||||
}()
|
||||
go func() {
|
||||
logger.Info("config center grpc listening", "address", cfg.GRPCAddr, "tls", cfg.GRPCTLSCertFile != "")
|
||||
serverErrors <- grpcServer.Serve(grpcListener)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Info("shutting down config center")
|
||||
case err := <-serverErrors:
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("http server stopped", "error", err)
|
||||
if err != nil && err != http.ErrServerClosed && !errors.Is(err, grpc.ErrServerStopped) {
|
||||
logger.Error("server stopped", "error", err)
|
||||
}
|
||||
}
|
||||
stop()
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
healthServer.Shutdown()
|
||||
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Error("graceful shutdown", "error", err)
|
||||
}
|
||||
grpcStopped := make(chan struct{})
|
||||
go func() {
|
||||
grpcServer.GracefulStop()
|
||||
close(grpcStopped)
|
||||
}()
|
||||
select {
|
||||
case <-grpcStopped:
|
||||
case <-shutdownCtx.Done():
|
||||
grpcServer.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func grpcOptions(cfg config.Config, api *grpcapi.Server) ([]grpc.ServerOption, error) {
|
||||
options := []grpc.ServerOption{
|
||||
grpc.UnaryInterceptor(api.UnaryInterceptor),
|
||||
grpc.StreamInterceptor(api.StreamInterceptor),
|
||||
}
|
||||
if (cfg.GRPCTLSCertFile == "") != (cfg.GRPCTLSKeyFile == "") {
|
||||
return nil, fmt.Errorf("GRPC_TLS_CERT_FILE and GRPC_TLS_KEY_FILE must be configured together")
|
||||
}
|
||||
if cfg.GRPCTLSCertFile != "" {
|
||||
transportCredentials, err := credentials.NewServerTLSFromFile(cfg.GRPCTLSCertFile, cfg.GRPCTLSKeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load grpc TLS certificate: %w", err)
|
||||
}
|
||||
options = append(options, grpc.Creds(transportCredentials))
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func openStore(ctx context.Context, cfg config.Config, logger *slog.Logger) (storepkg.Store, error) {
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "18080:8080"
|
||||
|
||||
web:
|
||||
build:
|
||||
|
||||
79
docs/adr/0001-runtime-revision-watch.md
Normal file
79
docs/adr/0001-runtime-revision-watch.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# ADR-0001:Runtime Revision、Watch 与 Outbox 一致性模型
|
||||
|
||||
状态:Accepted
|
||||
日期:2026-08-30
|
||||
|
||||
## 背景
|
||||
|
||||
Config Center 同时存在 PostgreSQL 控制面、Transactional Outbox、etcd Runtime Store、REST/SSE 以及后续 gRPC/SDK transport。若各层分别使用 release version、outbox id 或本地计数作为 revision,会导致断线恢复、compact 处理以及跨 transport 一致性无法定义。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. Runtime revision 的唯一来源
|
||||
|
||||
对外暴露的 `revision` 统一使用 **etcd MVCC revision**。
|
||||
|
||||
- PostgreSQL release version 只表示某个配置作用域的发布版本,不作为 Watch cursor。
|
||||
- Outbox ID 只表示可靠投递队列中的记录顺序,不作为 Watch cursor。
|
||||
- etcd Runtime Store 的 `Put` 使用单个事务同时写配置 key 与 `__meta`,事务返回的 header revision 为该次发布的 runtime revision。
|
||||
- 配置 key 的 `ModRevision` 是该配置快照实际生效的 revision;`Get` 返回该值。
|
||||
- Outbox worker 在写入 etcd 成功后,将该 etcd revision 回写到 release/outbox 状态,用于审计与排障。
|
||||
|
||||
内存 Runtime Store 仅用于开发和测试,但必须模拟相同的单调 revision、历史回放与 compact 行为。
|
||||
|
||||
### 2. Watch cursor 语义
|
||||
|
||||
`start_revision` 表示客户端希望接收的**第一个 revision(inclusive)**。
|
||||
|
||||
- `start_revision == 0`:新订阅。服务端先读取一致性完整快照 `revision=N`,发送 `FULL_SYNC`,随后从 `N+1` 开始 Watch。
|
||||
- `start_revision > 0`:重连。服务端从该 revision 开始 Watch,不主动跳到最新快照。
|
||||
- SDK 在正常断线重连时应保存最后成功处理的 revision `R`,并使用 `R+1` 作为新的 `start_revision`。
|
||||
|
||||
### 3. 瞬时断线
|
||||
|
||||
Watch 因网络错误、连接关闭等瞬时故障中断时:
|
||||
|
||||
1. 保留当前 `next_revision`;
|
||||
2. 重建 Watch,并继续从同一个 `next_revision` 监听;
|
||||
3. 不允许先 `Get latest` 再从 `latest+1` 继续,因为这会跳过断线期间仍可从 MVCC 历史回放的事件。
|
||||
|
||||
因此,只要目标 revision 尚未被 compact,断线恢复不会丢失可回放事件。
|
||||
|
||||
### 4. Revision compact
|
||||
|
||||
当 Runtime Store 明确返回 `ErrRevisionCompacted` 时,原 cursor 已无法完整回放。此时允许退化为状态恢复:
|
||||
|
||||
1. 读取当前完整快照,得到 `snapshot_revision=N`;
|
||||
2. 发送 `FULL_SYNC(revision=N)`;
|
||||
3. 从 `N+1` 继续 Watch。
|
||||
|
||||
这保证 compact 后客户端恢复到一个完整、一致的最新状态。compact 前被删除的中间事件无法再重放,因此服务端必须通过 `FULL_SYNC` 明确表达该语义,而不能伪装成普通 `UPDATED`。
|
||||
|
||||
### 5. Outbox 与 Runtime 的职责边界
|
||||
|
||||
权威关系如下:
|
||||
|
||||
`PostgreSQL transaction -> Release + Outbox -> Outbox Worker -> etcd transaction -> Runtime revision`
|
||||
|
||||
- PostgreSQL 是管理面、审计和发布记录的权威来源。
|
||||
- Outbox 是“发布记录最终进入 Runtime”的可靠投递机制,不是 Runtime Watch 事件源。
|
||||
- etcd 是运行时配置读取和 Watch 的权威来源。
|
||||
- REST/SSE/gRPC/Go SDK/Python SDK 必须共享同一套 etcd revision 语义。
|
||||
|
||||
### 6. 灰度规则
|
||||
|
||||
灰度规则当前保存在控制面 Store,不一定改变配置 key 的 etcd revision。因此灰度规则变化允许通过 `Force` refresh 触发相同 runtime revision 的重新计算。
|
||||
|
||||
客户端不得把“相同 revision 的灰度 refresh”误判为新的 Runtime revision。后续 gRPC 协议若需要完整表达该场景,应增加明确的 refresh/full-sync 语义,而不是人为生成新的 etcd revision。
|
||||
|
||||
## 已落实的代码约束
|
||||
|
||||
- `runtime.ErrRevisionCompacted` 作为 Runtime Store 的统一 compact 错误。
|
||||
- etcd Runtime Store 将 etcd compact 响应转换为统一错误,并保留 compact revision。
|
||||
- memory Runtime Store 模拟有限历史与 compact。
|
||||
- WatchHub 对普通断线从原 `next_revision` 重试;仅 compact 时重新读取完整快照并从 `snapshot_revision+1` 恢复。
|
||||
- WatchHub 的初始化和 compact 恢复快照使用 `FULL_SYNC`。
|
||||
|
||||
## 后续约束
|
||||
|
||||
正式 gRPC 实现、SSE reconnect 以及 Go/Python SDK transport 必须遵守本 ADR。任何修改 revision 来源或 `start_revision` 语义的变更,都需要新的 ADR,而不能在单个 transport 中单独修改。
|
||||
8
go.mod
8
go.mod
@@ -4,7 +4,11 @@ go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v5 v5.7.2
|
||||
go.etcd.io/etcd/api/v3 v3.6.7
|
||||
go.etcd.io/etcd/client/v3 v3.6.7
|
||||
golang.org/x/crypto v0.42.0
|
||||
google.golang.org/grpc v1.71.1
|
||||
google.golang.org/protobuf v1.36.5
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -16,17 +20,13 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.7 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/grpc v1.71.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
|
||||
330
internal/api/grpcapi/server.go
Normal file
330
internal/api/grpcapi/server.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
authpkg "github.com/longpeng/configcenter/internal/auth"
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
"github.com/longpeng/configcenter/internal/gray"
|
||||
metricspkg "github.com/longpeng/configcenter/internal/metrics"
|
||||
runtimepkg "github.com/longpeng/configcenter/internal/runtime"
|
||||
"github.com/longpeng/configcenter/internal/store"
|
||||
"github.com/longpeng/configcenter/internal/watch"
|
||||
configcenterv1 "github.com/longpeng/configcenter/pkg/proto/v1"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
configcenterv1.UnimplementedConfigServiceServer
|
||||
configcenterv1.UnimplementedAdminServiceServer
|
||||
|
||||
store store.Store
|
||||
runtime runtimepkg.Store
|
||||
hub *watch.Hub
|
||||
authorizer *authpkg.Manager
|
||||
metrics *metricspkg.Collector
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func New(repository store.Store, runtimeStore runtimepkg.Store, hub *watch.Hub, authorizer *authpkg.Manager, metrics *metricspkg.Collector, logger *slog.Logger) *Server {
|
||||
return &Server{
|
||||
store: repository,
|
||||
runtime: runtimeStore,
|
||||
hub: hub,
|
||||
authorizer: authorizer,
|
||||
metrics: metrics,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Register(registrar grpc.ServiceRegistrar) {
|
||||
configcenterv1.RegisterConfigServiceServer(registrar, s)
|
||||
configcenterv1.RegisterAdminServiceServer(registrar, s)
|
||||
}
|
||||
|
||||
func (s *Server) UnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (response any, err error) {
|
||||
started := time.Now()
|
||||
defer func() {
|
||||
s.metrics.ObserveGRPC(info.FullMethod, status.Code(err).String(), time.Since(started))
|
||||
}()
|
||||
if !publicMethod(info.FullMethod) {
|
||||
ctx, err = s.authenticateContext(ctx)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
}
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Server) StreamInterceptor(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) {
|
||||
started := time.Now()
|
||||
defer func() {
|
||||
s.metrics.ObserveGRPC(info.FullMethod, status.Code(err).String(), time.Since(started))
|
||||
}()
|
||||
if publicMethod(info.FullMethod) {
|
||||
return handler(srv, stream)
|
||||
}
|
||||
ctx, err := s.authenticateContext(stream.Context())
|
||||
if err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
return handler(srv, &contextServerStream{ServerStream: stream, ctx: ctx})
|
||||
}
|
||||
|
||||
func (s *Server) GetConfig(ctx context.Context, request *configcenterv1.GetConfigRequest) (*configcenterv1.GetConfigResponse, error) {
|
||||
key, appID, namespaceID, envID, err := s.resolveScope(ctx, request.GetEnv(), request.GetApp(), request.GetNamespace())
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
if err := s.authorizer.RequireAppRole(ctx, appID, authpkg.RoleViewer); err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
current, err := s.runtime.Get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, s.internal("grpc get runtime config", err)
|
||||
}
|
||||
rules, err := s.store.ListGrayRules(ctx, appID, namespaceID, envID)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
current.Items, current.GrayRuleIDs = gray.Apply(current.Items, rules, gray.Target{IP: strings.TrimSpace(request.GetIp()), Instance: strings.TrimSpace(request.GetInstance())})
|
||||
s.metrics.RuntimeRead()
|
||||
s.metrics.GrayMatched(len(current.GrayRuleIDs))
|
||||
return &configcenterv1.GetConfigResponse{
|
||||
Items: protoItems(current.Items),
|
||||
Revision: current.Revision,
|
||||
ReleaseVersion: int64(current.ReleaseVersion),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) WatchConfig(request *configcenterv1.WatchConfigRequest, stream grpc.ServerStreamingServer[configcenterv1.ConfigEvent]) error {
|
||||
if request.GetStartRevision() < 0 {
|
||||
return status.Error(codes.InvalidArgument, "start_revision must be non-negative")
|
||||
}
|
||||
ctx := stream.Context()
|
||||
key, appID, namespaceID, envID, err := s.resolveScope(ctx, request.GetEnv(), request.GetApp(), request.GetNamespace())
|
||||
if err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
if err := s.authorizer.RequireAppRole(ctx, appID, authpkg.RoleViewer); err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
|
||||
s.metrics.WatcherAdded()
|
||||
defer s.metrics.WatcherRemoved()
|
||||
|
||||
updates := runtimepkg.StreamSnapshots(ctx, s.runtime, key, request.GetStartRevision())
|
||||
refreshes, unsubscribe := s.hub.Subscribe(key, watch.Scope{ApplicationID: appID, NamespaceID: namespaceID, EnvironmentID: envID})
|
||||
defer unsubscribe()
|
||||
target := gray.Target{IP: strings.TrimSpace(request.GetIp()), Instance: strings.TrimSpace(request.GetInstance())}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case event, open := <-updates:
|
||||
if !open {
|
||||
return nil
|
||||
}
|
||||
if err := s.sendConfigEvent(ctx, stream, event, appID, namespaceID, envID, target); err != nil {
|
||||
return err
|
||||
}
|
||||
case event, open := <-refreshes:
|
||||
if !open {
|
||||
return nil
|
||||
}
|
||||
if !event.Force {
|
||||
continue
|
||||
}
|
||||
if err := s.sendConfigEvent(ctx, stream, event, appID, namespaceID, envID, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) PublishConfig(ctx context.Context, request *configcenterv1.PublishRequest) (*configcenterv1.PublishResponse, error) {
|
||||
if request.GetAppId() <= 0 || request.GetNamespaceId() <= 0 || request.GetEnvId() <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "env_id, app_id and namespace_id must be positive")
|
||||
}
|
||||
if err := s.authorizer.RequireAppRole(ctx, request.GetAppId(), authpkg.RoleAppOwner); err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
release, err := s.store.Publish(ctx, domain.PublishRequest{
|
||||
EnvironmentID: request.GetEnvId(),
|
||||
AppID: request.GetAppId(),
|
||||
NamespaceID: request.GetNamespaceId(),
|
||||
Comment: strings.TrimSpace(request.GetComment()),
|
||||
Operator: actor(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
s.metrics.PublishAccepted()
|
||||
return &configcenterv1.PublishResponse{ReleaseId: release.ID, ReleaseVersion: int64(release.Version), Status: release.Status}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RollbackConfig(ctx context.Context, request *configcenterv1.RollbackRequest) (*configcenterv1.RollbackResponse, error) {
|
||||
if request.GetAppId() <= 0 || request.GetNamespaceId() <= 0 || request.GetEnvId() <= 0 || request.GetTargetVersion() <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "env_id, app_id, namespace_id and target_version must be positive")
|
||||
}
|
||||
if err := s.authorizer.RequireAppRole(ctx, request.GetAppId(), authpkg.RoleAppOwner); err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
release, err := s.store.Rollback(ctx, domain.RollbackRequest{
|
||||
EnvironmentID: request.GetEnvId(),
|
||||
AppID: request.GetAppId(),
|
||||
NamespaceID: request.GetNamespaceId(),
|
||||
TargetVersion: int(request.GetTargetVersion()),
|
||||
Operator: actor(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
return &configcenterv1.RollbackResponse{ReleaseId: release.ID, NewReleaseVersion: int64(release.Version), Status: release.Status}, nil
|
||||
}
|
||||
|
||||
func (s *Server) sendConfigEvent(ctx context.Context, stream grpc.ServerStreamingServer[configcenterv1.ConfigEvent], event domain.ConfigEvent, appID, namespaceID, envID int64, target gray.Target) error {
|
||||
rules, err := s.store.ListGrayRules(ctx, appID, namespaceID, envID)
|
||||
if err != nil {
|
||||
return mapError(err)
|
||||
}
|
||||
items, matched := gray.Apply(event.Items, rules, target)
|
||||
s.metrics.GrayMatched(len(matched))
|
||||
eventType := configcenterv1.ConfigEvent_UPDATED
|
||||
if event.Type == "FULL_SYNC" {
|
||||
eventType = configcenterv1.ConfigEvent_FULL_SYNC
|
||||
}
|
||||
if err := stream.Send(&configcenterv1.ConfigEvent{Type: eventType, Items: protoItems(items), Revision: event.Revision}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) resolveScope(ctx context.Context, env, app, namespace string) (string, int64, int64, int64, error) {
|
||||
env = strings.ToLower(strings.TrimSpace(env))
|
||||
app = strings.TrimSpace(app)
|
||||
namespace = strings.TrimSpace(namespace)
|
||||
if !validCode(env) || !validCode(app) || !validCode(namespace) {
|
||||
return "", 0, 0, 0, status.Error(codes.InvalidArgument, "env, app and namespace have invalid format")
|
||||
}
|
||||
appID, namespaceID, envID, err := s.store.ResolveScope(ctx, env, app, namespace)
|
||||
if err != nil {
|
||||
return "", 0, 0, 0, err
|
||||
}
|
||||
return fmt.Sprintf("/config/%s/%s/%s", env, app, namespace), appID, namespaceID, envID, nil
|
||||
}
|
||||
|
||||
func (s *Server) authenticateContext(ctx context.Context) (context.Context, error) {
|
||||
values, _ := metadata.FromIncomingContext(ctx)
|
||||
principal, err := s.authorizer.AuthenticateToken(bearerToken(values.Get("authorization")))
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
if !s.authorizer.Enabled() {
|
||||
if users := values.Get("x-user"); len(users) > 0 {
|
||||
if username := strings.TrimSpace(users[0]); username != "" && len(username) <= 64 {
|
||||
principal.Username = username
|
||||
principal.DisplayName = username
|
||||
}
|
||||
}
|
||||
}
|
||||
return authpkg.WithPrincipal(ctx, principal), nil
|
||||
}
|
||||
|
||||
func bearerToken(values []string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) > len("Bearer ") && strings.EqualFold(value[:len("Bearer ")], "Bearer ") {
|
||||
return strings.TrimSpace(value[len("Bearer "):])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func protoItems(items map[string]string) []*configcenterv1.ConfigItem {
|
||||
keys := make([]string, 0, len(items))
|
||||
for key := range items {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
result := make([]*configcenterv1.ConfigItem, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
result = append(result, &configcenterv1.ConfigItem{Key: key, Value: items[key]})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func actor(ctx context.Context) string {
|
||||
if principal, ok := authpkg.Principal(ctx); ok && principal.Username != "" {
|
||||
return principal.Username
|
||||
}
|
||||
return "admin"
|
||||
}
|
||||
|
||||
func validCode(value string) bool {
|
||||
if value == "" || len(value) > 64 {
|
||||
return false
|
||||
}
|
||||
for _, character := range value {
|
||||
if unicode.IsLetter(character) || unicode.IsDigit(character) || character == '-' || character == '_' || character == '.' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func publicMethod(method string) bool {
|
||||
return strings.HasPrefix(method, "/grpc.health.v1.Health/")
|
||||
}
|
||||
|
||||
func mapError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := status.FromError(err); ok {
|
||||
if status.Code(err) != codes.Unknown {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, authpkg.ErrUnauthorized):
|
||||
return status.Error(codes.Unauthenticated, "authentication required or token expired")
|
||||
case errors.Is(err, authpkg.ErrForbidden):
|
||||
return status.Error(codes.PermissionDenied, "permission denied")
|
||||
case errors.Is(err, store.ErrNotFound):
|
||||
return status.Error(codes.NotFound, "resource not found")
|
||||
case errors.Is(err, store.ErrConflict):
|
||||
return status.Error(codes.AlreadyExists, "resource already exists")
|
||||
case errors.Is(err, store.ErrNoPendingChange):
|
||||
return status.Error(codes.FailedPrecondition, "no pending configuration changes")
|
||||
case errors.Is(err, store.ErrInvalidRollback):
|
||||
return status.Error(codes.FailedPrecondition, "invalid rollback target")
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) internal(operation string, err error) error {
|
||||
s.logger.Error(operation, "error", err)
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
|
||||
type contextServerStream struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (s *contextServerStream) Context() context.Context { return s.ctx }
|
||||
278
internal/api/grpcapi/server_test.go
Normal file
278
internal/api/grpcapi/server_test.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
|
||||
authpkg "github.com/longpeng/configcenter/internal/auth"
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
"github.com/longpeng/configcenter/internal/metrics"
|
||||
memoryruntime "github.com/longpeng/configcenter/internal/runtime/memory"
|
||||
memory_store "github.com/longpeng/configcenter/internal/store/memory"
|
||||
"github.com/longpeng/configcenter/internal/watch"
|
||||
configcenterv1 "github.com/longpeng/configcenter/pkg/proto/v1"
|
||||
)
|
||||
|
||||
func TestConfigServiceGetAndWatchFromRevision(t *testing.T) {
|
||||
testServer := newTestServer(t, false, 8*time.Hour)
|
||||
putRuntime(t, testServer.runtime, 1, map[string]string{"feature": "one"})
|
||||
putRuntime(t, testServer.runtime, 2, map[string]string{"feature": "two"})
|
||||
putRuntime(t, testServer.runtime, 3, map[string]string{"feature": "three"})
|
||||
|
||||
response, err := testServer.config.GetConfig(context.Background(), &configcenterv1.GetConfigRequest{
|
||||
Env: "DEV", App: "demo-service", Namespace: "application",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.GetRevision() != 3 || response.GetReleaseVersion() != 3 || itemValue(response.GetItems(), "feature") != "three" {
|
||||
t.Fatalf("unexpected get response: %#v", response)
|
||||
}
|
||||
|
||||
watchContext, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
stream, err := testServer.config.WatchConfig(watchContext, &configcenterv1.WatchConfigRequest{
|
||||
Env: "DEV", App: "demo-service", Namespace: "application", StartRevision: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := stream.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := stream.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.GetRevision() != 2 || first.GetType() != configcenterv1.ConfigEvent_UPDATED || itemValue(first.GetItems(), "feature") != "two" {
|
||||
t.Fatalf("unexpected first replay event: %#v", first)
|
||||
}
|
||||
if second.GetRevision() != 3 || second.GetType() != configcenterv1.ConfigEvent_UPDATED || itemValue(second.GetItems(), "feature") != "three" {
|
||||
t.Fatalf("unexpected second replay event: %#v", second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchFallsBackToFullSyncAfterCompaction(t *testing.T) {
|
||||
testServer := newTestServer(t, false, 8*time.Hour)
|
||||
for version := 1; version <= 257; version++ {
|
||||
putRuntime(t, testServer.runtime, version, map[string]string{"version": string(rune('A' + (version % 26)))})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
stream, err := testServer.config.WatchConfig(ctx, &configcenterv1.WatchConfigRequest{
|
||||
Env: "DEV", App: "demo-service", Namespace: "application", StartRevision: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event, err := stream.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event.GetType() != configcenterv1.ConfigEvent_FULL_SYNC || event.GetRevision() != 257 {
|
||||
t.Fatalf("unexpected compact recovery event: %#v", event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchReconnectsFromLastSeenRevisionPlusOne(t *testing.T) {
|
||||
testServer := newTestServer(t, false, 8*time.Hour)
|
||||
putRuntime(t, testServer.runtime, 1, map[string]string{"feature": "one"})
|
||||
|
||||
firstContext, cancelFirst := context.WithCancel(context.Background())
|
||||
firstStream, err := testServer.config.WatchConfig(firstContext, &configcenterv1.WatchConfigRequest{
|
||||
Env: "DEV", App: "demo-service", Namespace: "application",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
initial, err := firstStream.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if initial.GetType() != configcenterv1.ConfigEvent_FULL_SYNC || initial.GetRevision() != 1 {
|
||||
t.Fatalf("unexpected initial watch event: %#v", initial)
|
||||
}
|
||||
cancelFirst()
|
||||
|
||||
putRuntime(t, testServer.runtime, 2, map[string]string{"feature": "two"})
|
||||
putRuntime(t, testServer.runtime, 3, map[string]string{"feature": "three"})
|
||||
|
||||
secondContext, cancelSecond := context.WithCancel(context.Background())
|
||||
defer cancelSecond()
|
||||
secondStream, err := testServer.config.WatchConfig(secondContext, &configcenterv1.WatchConfigRequest{
|
||||
Env: "DEV", App: "demo-service", Namespace: "application", StartRevision: initial.GetRevision() + 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := secondStream.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
third, err := secondStream.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second.GetType() != configcenterv1.ConfigEvent_UPDATED || second.GetRevision() != 2 || itemValue(second.GetItems(), "feature") != "two" {
|
||||
t.Fatalf("unexpected event after reconnect: %#v", second)
|
||||
}
|
||||
if third.GetType() != configcenterv1.ConfigEvent_UPDATED || third.GetRevision() != 3 || itemValue(third.GetItems(), "feature") != "three" {
|
||||
t.Fatalf("unexpected second event after reconnect: %#v", third)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminServicePublishAndRollbackUseAuthenticatedActor(t *testing.T) {
|
||||
testServer := newTestServer(t, false, 8*time.Hour)
|
||||
if _, err := testServer.repository.UpdateConfigItem(context.Background(), 30, domain.ConfigItem{Value: "9090", UpdatedBy: "editor"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("x-user", "grpc-admin"))
|
||||
published, err := testServer.admin.PublishConfig(ctx, &configcenterv1.PublishRequest{
|
||||
EnvId: 1, AppId: 10, NamespaceId: 20, Comment: "grpc publish", Operator: "spoofed",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if published.GetReleaseVersion() != 2 || published.GetStatus() != "pending" {
|
||||
t.Fatalf("unexpected publish response: %#v", published)
|
||||
}
|
||||
release, err := testServer.repository.GetRelease(context.Background(), published.GetReleaseId())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if release.Operator != "grpc-admin" {
|
||||
t.Fatalf("client supplied operator was trusted: %#v", release)
|
||||
}
|
||||
|
||||
rolledBack, err := testServer.admin.RollbackConfig(ctx, &configcenterv1.RollbackRequest{
|
||||
EnvId: 1, AppId: 10, NamespaceId: 20, TargetVersion: 1, Operator: "spoofed",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rolledBack.GetNewReleaseVersion() != 3 || rolledBack.GetStatus() != "pending" {
|
||||
t.Fatalf("unexpected rollback response: %#v", rolledBack)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationRejectsMissingRoleAndExpiredToken(t *testing.T) {
|
||||
const testPassword = "correct-horse-battery-staple"
|
||||
testServer := newTestServer(t, true, 8*time.Hour)
|
||||
user, err := testServer.authorizer.CreateUser(context.Background(), domain.User{Username: "reader", DisplayName: "Reader"}, testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.ID == 0 {
|
||||
t.Fatal("user was not created")
|
||||
}
|
||||
token, _, err := testServer.authorizer.Login(context.Background(), "reader", testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = testServer.config.GetConfig(bearerContext(token), &configcenterv1.GetConfigRequest{Env: "DEV", App: "demo-service", Namespace: "application"})
|
||||
if status.Code(err) != codes.PermissionDenied {
|
||||
t.Fatalf("expected permission denied, got %v", err)
|
||||
}
|
||||
|
||||
expiring := newTestServer(t, true, time.Millisecond)
|
||||
adminToken, _, err := expiring.authorizer.Login(context.Background(), "admin", testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
_, err = expiring.config.GetConfig(bearerContext(adminToken), &configcenterv1.GetConfigRequest{Env: "DEV", App: "demo-service", Namespace: "application"})
|
||||
if status.Code(err) != codes.Unauthenticated {
|
||||
t.Fatalf("expected unauthenticated for expired token, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type grpcTestServer struct {
|
||||
config configcenterv1.ConfigServiceClient
|
||||
admin configcenterv1.AdminServiceClient
|
||||
runtime *memoryruntime.Store
|
||||
repository *memory_store.Store
|
||||
authorizer *authpkg.Manager
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, authEnabled bool, ttl time.Duration) grpcTestServer {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
repository := memory_store.New(true)
|
||||
runtimeStore := memoryruntime.New()
|
||||
authorizer, err := authpkg.New(repository, authEnabled, strings.Repeat("s", 32), ttl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if authEnabled {
|
||||
if err := authorizer.Bootstrap(ctx, "admin", "correct-horse-battery-staple", "Administrator"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
collector := metrics.New()
|
||||
hub := watch.New(ctx, runtimeStore)
|
||||
api := New(repository, runtimeStore, hub, authorizer, collector, slog.Default())
|
||||
server := grpc.NewServer(grpc.UnaryInterceptor(api.UnaryInterceptor), grpc.StreamInterceptor(api.StreamInterceptor))
|
||||
api.Register(server)
|
||||
listener := bufconn.Listen(1 << 20)
|
||||
go func() { _ = server.Serve(listener) }()
|
||||
t.Cleanup(server.Stop)
|
||||
t.Cleanup(func() { _ = listener.Close() })
|
||||
|
||||
connection, err := grpc.NewClient(
|
||||
"passthrough:///bufnet",
|
||||
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = connection.Close() })
|
||||
return grpcTestServer{
|
||||
config: configcenterv1.NewConfigServiceClient(connection),
|
||||
admin: configcenterv1.NewAdminServiceClient(connection),
|
||||
runtime: runtimeStore,
|
||||
repository: repository,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
func putRuntime(t *testing.T, runtimeStore *memoryruntime.Store, version int, items map[string]string) {
|
||||
t.Helper()
|
||||
payload, err := json.Marshal(items)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := runtimeStore.Put(context.Background(), "/config/dev/demo-service/application", payload, domain.Release{ID: int64(version), Version: version, Operator: "test", Time: time.Now()}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func itemValue(items []*configcenterv1.ConfigItem, key string) string {
|
||||
for _, item := range items {
|
||||
if item.GetKey() == key {
|
||||
return item.GetValue()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func bearerContext(token string) context.Context {
|
||||
return metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorization", "Bearer "+token))
|
||||
}
|
||||
@@ -105,14 +105,24 @@ func (m *Manager) CreateUser(ctx context.Context, input domain.User, password st
|
||||
}
|
||||
|
||||
func (m *Manager) AuthenticateRequest(r *http.Request) (domain.Principal, error) {
|
||||
if !m.enabled {
|
||||
return domain.Principal{Username: "admin", DisplayName: "Development Admin", IsAdmin: true}, nil
|
||||
}
|
||||
header := strings.TrimSpace(r.Header.Get("Authorization"))
|
||||
if !m.enabled {
|
||||
return m.AuthenticateToken("")
|
||||
}
|
||||
if !strings.HasPrefix(header, "Bearer ") {
|
||||
return domain.Principal{}, ErrUnauthorized
|
||||
}
|
||||
return m.parse(strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")))
|
||||
return m.AuthenticateToken(strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")))
|
||||
}
|
||||
|
||||
func (m *Manager) AuthenticateToken(token string) (domain.Principal, error) {
|
||||
if !m.enabled {
|
||||
return domain.Principal{Username: "admin", DisplayName: "Development Admin", IsAdmin: true}, nil
|
||||
}
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return domain.Principal{}, ErrUnauthorized
|
||||
}
|
||||
return m.parse(strings.TrimSpace(token))
|
||||
}
|
||||
|
||||
func WithPrincipal(ctx context.Context, principal domain.Principal) context.Context {
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
|
||||
type Config struct {
|
||||
HTTPAddr string
|
||||
GRPCAddr string
|
||||
GRPCTLSCertFile string
|
||||
GRPCTLSKeyFile string
|
||||
DatabaseURL string
|
||||
EtcdEndpoints []string
|
||||
EtcdDialTimeout time.Duration
|
||||
@@ -32,6 +35,9 @@ type Config struct {
|
||||
func Load() Config {
|
||||
return Config{
|
||||
HTTPAddr: env("HTTP_ADDR", ":8080"),
|
||||
GRPCAddr: env("GRPC_ADDR", ":9091"),
|
||||
GRPCTLSCertFile: strings.TrimSpace(os.Getenv("GRPC_TLS_CERT_FILE")),
|
||||
GRPCTLSKeyFile: strings.TrimSpace(os.Getenv("GRPC_TLS_KEY_FILE")),
|
||||
DatabaseURL: strings.TrimSpace(os.Getenv("DATABASE_URL")),
|
||||
EtcdEndpoints: csv(os.Getenv("ETCD_ENDPOINTS")),
|
||||
EtcdDialTimeout: duration("ETCD_DIAL_TIMEOUT", 5*time.Second),
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
type Collector struct {
|
||||
mu sync.Mutex
|
||||
http map[httpKey]httpValue
|
||||
grpc map[grpcKey]httpValue
|
||||
|
||||
publishAccepted atomic.Uint64
|
||||
outboxApplied atomic.Uint64
|
||||
@@ -31,6 +32,11 @@ type httpKey struct {
|
||||
status int
|
||||
}
|
||||
|
||||
type grpcKey struct {
|
||||
method string
|
||||
code string
|
||||
}
|
||||
|
||||
type httpValue struct {
|
||||
count uint64
|
||||
durationSum float64
|
||||
@@ -39,7 +45,9 @@ type httpValue struct {
|
||||
|
||||
var httpDurationBuckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5}
|
||||
|
||||
func New() *Collector { return &Collector{http: make(map[httpKey]httpValue)} }
|
||||
func New() *Collector {
|
||||
return &Collector{http: make(map[httpKey]httpValue), grpc: make(map[grpcKey]httpValue)}
|
||||
}
|
||||
|
||||
func (c *Collector) ObserveHTTP(method, route string, status int, duration time.Duration) {
|
||||
if route == "" {
|
||||
@@ -63,6 +71,25 @@ func (c *Collector) ObserveHTTP(method, route string, status int, duration time.
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Collector) ObserveGRPC(method, code string, duration time.Duration) {
|
||||
key := grpcKey{method: method, code: code}
|
||||
c.mu.Lock()
|
||||
value := c.grpc[key]
|
||||
if value.buckets == nil {
|
||||
value.buckets = make([]uint64, len(httpDurationBuckets))
|
||||
}
|
||||
value.count++
|
||||
seconds := duration.Seconds()
|
||||
value.durationSum += seconds
|
||||
for index, upperBound := range httpDurationBuckets {
|
||||
if seconds <= upperBound {
|
||||
value.buckets[index]++
|
||||
}
|
||||
}
|
||||
c.grpc[key] = value
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Collector) PublishAccepted() { c.publishAccepted.Add(1) }
|
||||
func (c *Collector) OutboxApplied() { c.outboxApplied.Add(1) }
|
||||
func (c *Collector) OutboxFailed() { c.outboxFailed.Add(1) }
|
||||
@@ -113,12 +140,38 @@ func (c *Collector) Handler(repository store.Store) http.HandlerFunc {
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
writeHelp(&output, "configcenter_grpc_requests_total", "gRPC requests by method and status code", "counter")
|
||||
writeHelp(&output, "configcenter_grpc_request_duration_seconds", "gRPC request duration by method and status code", "histogram")
|
||||
c.mu.Lock()
|
||||
grpcKeys := make([]grpcKey, 0, len(c.grpc))
|
||||
for key := range c.grpc {
|
||||
grpcKeys = append(grpcKeys, key)
|
||||
}
|
||||
sort.Slice(grpcKeys, func(i, j int) bool {
|
||||
if grpcKeys[i].method != grpcKeys[j].method {
|
||||
return grpcKeys[i].method < grpcKeys[j].method
|
||||
}
|
||||
return grpcKeys[i].code < grpcKeys[j].code
|
||||
})
|
||||
for _, key := range grpcKeys {
|
||||
value := c.grpc[key]
|
||||
labels := fmt.Sprintf(`method="%s",code="%s"`, escape(key.method), escape(key.code))
|
||||
fmt.Fprintf(&output, "configcenter_grpc_requests_total{%s} %d\n", labels, value.count)
|
||||
for index, upperBound := range httpDurationBuckets {
|
||||
fmt.Fprintf(&output, "configcenter_grpc_request_duration_seconds_bucket{%s,le=\"%s\"} %d\n", labels, strconv.FormatFloat(upperBound, 'g', -1, 64), value.buckets[index])
|
||||
}
|
||||
fmt.Fprintf(&output, "configcenter_grpc_request_duration_seconds_bucket{%s,le=\"+Inf\"} %d\n", labels, value.count)
|
||||
fmt.Fprintf(&output, "configcenter_grpc_request_duration_seconds_sum{%s} %s\n", labels, strconv.FormatFloat(value.durationSum, 'f', 6, 64))
|
||||
fmt.Fprintf(&output, "configcenter_grpc_request_duration_seconds_count{%s} %d\n", labels, value.count)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
writeCounter(&output, "configcenter_publish_accepted_total", "Accepted configuration releases", c.publishAccepted.Load())
|
||||
writeCounter(&output, "configcenter_outbox_applied_total", "Successfully applied outbox entries", c.outboxApplied.Load())
|
||||
writeCounter(&output, "configcenter_outbox_failed_attempts_total", "Failed outbox apply attempts", c.outboxFailed.Load())
|
||||
writeCounter(&output, "configcenter_runtime_reads_total", "Runtime configuration reads", c.runtimeReads.Load())
|
||||
writeCounter(&output, "configcenter_gray_rule_matches_total", "Gray rules matched during runtime reads and watches", c.grayMatches.Load())
|
||||
writeGauge(&output, "configcenter_watch_subscribers", "Current SSE watch subscribers", c.watchers.Load())
|
||||
writeGauge(&output, "configcenter_watch_subscribers", "Current SSE and gRPC watch subscribers", c.watchers.Load())
|
||||
writeGauge(&output, "configcenter_outbox_pending", "Pending outbox entries", stats.Pending)
|
||||
writeGauge(&output, "configcenter_outbox_processing", "Processing outbox entries", stats.Processing)
|
||||
writeGauge(&output, "configcenter_outbox_failed", "Terminally failed outbox entries", stats.Failed)
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
@@ -134,8 +136,12 @@ func (s *Store) Watch(ctx context.Context, key string, startRevision int64) <-ch
|
||||
defer close(result)
|
||||
for response := range watch {
|
||||
if err := response.Err(); err != nil {
|
||||
watchErr := err
|
||||
if response.CompactRevision > 0 || errors.Is(err, rpctypes.ErrCompacted) {
|
||||
watchErr = fmt.Errorf("%w: requested=%d compacted=%d", runtimepkg.ErrRevisionCompacted, startRevision, response.CompactRevision)
|
||||
}
|
||||
select {
|
||||
case result <- runtimepkg.WatchResult{Err: err}:
|
||||
case result <- runtimepkg.WatchResult{Err: watchErr, CompactRevision: response.CompactRevision}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package memory
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
@@ -32,6 +33,7 @@ type Store struct {
|
||||
subscribers map[int64]subscriber
|
||||
nextSubID int64
|
||||
history []historyEvent
|
||||
compactRev int64
|
||||
}
|
||||
|
||||
func New() *Store {
|
||||
@@ -62,6 +64,10 @@ func (s *Store) Put(_ context.Context, key string, payload []byte, release domai
|
||||
event := domain.ConfigEvent{Type: "UPDATED", Items: clone(items), Revision: revision}
|
||||
s.history = append(s.history, historyEvent{key: key, event: event})
|
||||
if len(s.history) > 256 {
|
||||
removed := s.history[:len(s.history)-256]
|
||||
if len(removed) > 0 {
|
||||
s.compactRev = removed[len(removed)-1].event.Revision
|
||||
}
|
||||
s.history = append([]historyEvent(nil), s.history[len(s.history)-256:]...)
|
||||
}
|
||||
for _, sub := range s.subscribers {
|
||||
@@ -88,8 +94,22 @@ func (s *Store) Get(_ context.Context, key string) (domain.RuntimeConfig, error)
|
||||
}
|
||||
|
||||
func (s *Store) Watch(ctx context.Context, key string, startRevision int64) <-chan runtimepkg.WatchResult {
|
||||
ch := make(chan runtimepkg.WatchResult, 16)
|
||||
s.mu.Lock()
|
||||
bufferSize := 16
|
||||
if len(s.history)+16 > bufferSize {
|
||||
bufferSize = len(s.history) + 16
|
||||
}
|
||||
ch := make(chan runtimepkg.WatchResult, bufferSize)
|
||||
if startRevision > 0 && startRevision <= s.compactRev {
|
||||
compactRevision := s.compactRev
|
||||
s.mu.Unlock()
|
||||
ch <- runtimepkg.WatchResult{
|
||||
Err: fmt.Errorf("%w: requested=%d compacted=%d", runtimepkg.ErrRevisionCompacted, startRevision, compactRevision),
|
||||
CompactRevision: compactRevision,
|
||||
}
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
s.nextSubID++
|
||||
id := s.nextSubID
|
||||
s.subscribers[id] = subscriber{key: key, ch: ch}
|
||||
|
||||
56
internal/runtime/memory/memory_test.go
Normal file
56
internal/runtime/memory/memory_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
runtimepkg "github.com/longpeng/configcenter/internal/runtime"
|
||||
)
|
||||
|
||||
func TestWatchReportsCompactedRevision(t *testing.T) {
|
||||
store := New()
|
||||
ctx := context.Background()
|
||||
payload, err := json.Marshal(map[string]string{"feature": "value"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for version := 1; version <= 257; version++ {
|
||||
if _, err := store.Put(ctx, "/config/PROD/orders/application", payload, domain.Release{Version: version}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
result := <-store.Watch(ctx, "/config/PROD/orders/application", 1)
|
||||
if !errors.Is(result.Err, runtimepkg.ErrRevisionCompacted) {
|
||||
t.Fatalf("expected compacted error, got %#v", result)
|
||||
}
|
||||
if result.CompactRevision != 1 {
|
||||
t.Fatalf("unexpected compact revision: %d", result.CompactRevision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchReplaysEveryAvailableRevision(t *testing.T) {
|
||||
store := New()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for version := 1; version <= 20; version++ {
|
||||
payload, err := json.Marshal(map[string]string{"version": string(rune('A' + version - 1))})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.Put(ctx, "/config/PROD/orders/application", payload, domain.Release{Version: version}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
stream := store.Watch(ctx, "/config/PROD/orders/application", 5)
|
||||
for revision := int64(5); revision <= 20; revision++ {
|
||||
result := <-stream
|
||||
if result.Err != nil || result.Event.Revision != revision {
|
||||
t.Fatalf("unexpected replay at revision %d: %#v", revision, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,17 @@ package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
)
|
||||
|
||||
var ErrRevisionCompacted = errors.New("runtime revision compacted")
|
||||
|
||||
type WatchResult struct {
|
||||
Event domain.ConfigEvent
|
||||
Err error
|
||||
Event domain.ConfigEvent
|
||||
Err error
|
||||
CompactRevision int64
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
|
||||
131
internal/runtime/stream.go
Normal file
131
internal/runtime/stream.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
streamInitialBackoff = 100 * time.Millisecond
|
||||
streamMaxBackoff = 5 * time.Second
|
||||
)
|
||||
|
||||
// StreamSnapshots exposes the runtime watch contract used by every transport.
|
||||
// startRevision is inclusive. A value <= 0 means a fresh subscription: emit a
|
||||
// FULL_SYNC snapshot first, then watch from snapshot revision + 1.
|
||||
func StreamSnapshots(ctx context.Context, store Store, key string, startRevision int64) <-chan domain.ConfigEvent {
|
||||
events := make(chan domain.ConfigEvent, 16)
|
||||
go func() {
|
||||
defer close(events)
|
||||
|
||||
nextRevision := startRevision
|
||||
if nextRevision <= 0 {
|
||||
current, ok := readSnapshot(ctx, store, key)
|
||||
if !ok || !sendEvent(ctx, events, fullSync(current)) {
|
||||
return
|
||||
}
|
||||
nextRevision = current.Revision + 1
|
||||
}
|
||||
|
||||
backoff := streamInitialBackoff
|
||||
for {
|
||||
stream := store.Watch(ctx, key, nextRevision)
|
||||
compacted := false
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case result, open := <-stream:
|
||||
if !open {
|
||||
goto reconnect
|
||||
}
|
||||
if result.Err != nil {
|
||||
compacted = errors.Is(result.Err, ErrRevisionCompacted)
|
||||
goto reconnect
|
||||
}
|
||||
if result.Event.Revision > 0 {
|
||||
if nextRevision > 0 && result.Event.Revision < nextRevision {
|
||||
continue
|
||||
}
|
||||
nextRevision = result.Event.Revision + 1
|
||||
}
|
||||
if !sendEvent(ctx, events, result.Event) {
|
||||
return
|
||||
}
|
||||
backoff = streamInitialBackoff
|
||||
}
|
||||
}
|
||||
|
||||
reconnect:
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if compacted {
|
||||
current, ok := readSnapshot(ctx, store, key)
|
||||
if !ok || !sendEvent(ctx, events, fullSync(current)) {
|
||||
return
|
||||
}
|
||||
nextRevision = current.Revision + 1
|
||||
backoff = streamInitialBackoff
|
||||
continue
|
||||
}
|
||||
if !waitForStreamRetry(ctx, backoff) {
|
||||
return
|
||||
}
|
||||
backoff = nextStreamBackoff(backoff)
|
||||
}
|
||||
}()
|
||||
return events
|
||||
}
|
||||
|
||||
func readSnapshot(ctx context.Context, store Store, key string) (domain.RuntimeConfig, bool) {
|
||||
backoff := streamInitialBackoff
|
||||
for {
|
||||
current, err := store.Get(ctx, key)
|
||||
if err == nil {
|
||||
return current, true
|
||||
}
|
||||
if !waitForStreamRetry(ctx, backoff) {
|
||||
return domain.RuntimeConfig{}, false
|
||||
}
|
||||
backoff = nextStreamBackoff(backoff)
|
||||
}
|
||||
}
|
||||
|
||||
func fullSync(current domain.RuntimeConfig) domain.ConfigEvent {
|
||||
return domain.ConfigEvent{Type: "FULL_SYNC", Items: current.Items, Revision: current.Revision}
|
||||
}
|
||||
|
||||
func sendEvent(ctx context.Context, target chan<- domain.ConfigEvent, event domain.ConfigEvent) bool {
|
||||
select {
|
||||
case target <- event:
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func waitForStreamRetry(ctx context.Context, delay time.Duration) bool {
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-timer.C:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func nextStreamBackoff(current time.Duration) time.Duration {
|
||||
if current >= streamMaxBackoff {
|
||||
return streamMaxBackoff
|
||||
}
|
||||
next := current * 2
|
||||
if next > streamMaxBackoff {
|
||||
return streamMaxBackoff
|
||||
}
|
||||
return next
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package watch
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/longpeng/configcenter/internal/domain"
|
||||
runtimepkg "github.com/longpeng/configcenter/internal/runtime"
|
||||
@@ -69,58 +68,8 @@ func (h *Hub) Subscribe(key string, scope Scope) (<-chan domain.ConfigEvent, fun
|
||||
}
|
||||
|
||||
func (h *Hub) run(ctx context.Context, key string, target *keyWatch) {
|
||||
nextRevision := int64(0)
|
||||
backoff := 100 * time.Millisecond
|
||||
for {
|
||||
current, err := h.runtime.Get(ctx, key)
|
||||
if err == nil {
|
||||
if current.Revision > 0 {
|
||||
nextRevision = current.Revision + 1
|
||||
}
|
||||
h.broadcast(key, target, domain.ConfigEvent{Type: "UPDATED", Items: current.Items, Revision: current.Revision})
|
||||
break
|
||||
}
|
||||
if !waitForRetry(ctx, backoff) {
|
||||
return
|
||||
}
|
||||
backoff = nextBackoff(backoff)
|
||||
}
|
||||
backoff = 100 * time.Millisecond
|
||||
for {
|
||||
stream := h.runtime.Watch(ctx, key, nextRevision)
|
||||
disconnected := false
|
||||
for !disconnected {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case result, open := <-stream:
|
||||
if !open || result.Err != nil {
|
||||
disconnected = true
|
||||
continue
|
||||
}
|
||||
if result.Event.Revision > 0 {
|
||||
if nextRevision > 0 && result.Event.Revision < nextRevision {
|
||||
continue
|
||||
}
|
||||
nextRevision = result.Event.Revision + 1
|
||||
}
|
||||
h.broadcast(key, target, result.Event)
|
||||
backoff = 100 * time.Millisecond
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if current, err := h.runtime.Get(ctx, key); err == nil {
|
||||
if current.Revision > 0 {
|
||||
nextRevision = current.Revision + 1
|
||||
}
|
||||
h.broadcast(key, target, domain.ConfigEvent{Type: "UPDATED", Items: current.Items, Revision: current.Revision})
|
||||
}
|
||||
if !waitForRetry(ctx, backoff) {
|
||||
return
|
||||
}
|
||||
backoff = nextBackoff(backoff)
|
||||
for event := range runtimepkg.StreamSnapshots(ctx, h.runtime, key, 0) {
|
||||
h.broadcast(key, target, event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,25 +116,3 @@ func (h *Hub) broadcast(key string, target *keyWatch, event domain.ConfigEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForRetry(ctx context.Context, delay time.Duration) bool {
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-timer.C:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func nextBackoff(current time.Duration) time.Duration {
|
||||
if current >= 5*time.Second {
|
||||
return 5 * time.Second
|
||||
}
|
||||
next := current * 2
|
||||
if next > 5*time.Second {
|
||||
return 5 * time.Second
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -35,12 +35,8 @@ func TestHubReconnectsFromNextRevision(t *testing.T) {
|
||||
t.Fatalf("unexpected first event: %#v", event)
|
||||
}
|
||||
|
||||
runtimeStore.setCurrent(domain.RuntimeConfig{Items: map[string]string{"feature": "two"}, Revision: 4})
|
||||
runtimeStore.setCurrent(domain.RuntimeConfig{Items: map[string]string{"feature": "four"}, Revision: 6})
|
||||
first <- runtimepkg.WatchResult{Err: errors.New("watch connection lost")}
|
||||
resync := receiveEvent(t, events)
|
||||
if resync.Revision != 4 || resync.Force || resync.Items["feature"] != "two" {
|
||||
t.Fatalf("unexpected reconnect resync: %#v", resync)
|
||||
}
|
||||
|
||||
waitForWatchCalls(t, runtimeStore, 2)
|
||||
calls := runtimeStore.watchRevisions()
|
||||
@@ -48,11 +44,47 @@ func TestHubReconnectsFromNextRevision(t *testing.T) {
|
||||
t.Fatalf("unexpected watch revisions: %v", calls)
|
||||
}
|
||||
|
||||
second <- runtimepkg.WatchResult{Event: domain.ConfigEvent{Type: "UPDATED", Items: map[string]string{"feature": "stale"}, Revision: 4}}
|
||||
second <- runtimepkg.WatchResult{Event: domain.ConfigEvent{Type: "UPDATED", Items: map[string]string{"feature": "three"}, Revision: 5}}
|
||||
second <- runtimepkg.WatchResult{Event: domain.ConfigEvent{Type: "UPDATED", Items: map[string]string{"feature": "four"}, Revision: 6}}
|
||||
if event := receiveEvent(t, events); event.Revision != 5 || event.Items["feature"] != "three" {
|
||||
t.Fatalf("unexpected resumed event: %#v", event)
|
||||
}
|
||||
if event := receiveEvent(t, events); event.Revision != 6 || event.Items["feature"] != "four" {
|
||||
t.Fatalf("unexpected second resumed event: %#v", event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubFallsBackToFullSyncOnlyWhenRevisionCompacted(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
first := make(chan runtimepkg.WatchResult, 4)
|
||||
second := make(chan runtimepkg.WatchResult, 4)
|
||||
runtimeStore := &fakeRuntimeStore{
|
||||
current: domain.RuntimeConfig{Items: map[string]string{"feature": "one"}, Revision: 3},
|
||||
streams: []chan runtimepkg.WatchResult{first, second},
|
||||
}
|
||||
hub := New(ctx, runtimeStore)
|
||||
events, unsubscribe := hub.Subscribe("/config/PROD/orders/application", Scope{ApplicationID: 1, NamespaceID: 2, EnvironmentID: 3})
|
||||
defer unsubscribe()
|
||||
|
||||
initial := receiveEvent(t, events)
|
||||
if initial.Type != "FULL_SYNC" || initial.Revision != 3 {
|
||||
t.Fatalf("unexpected initial event: %#v", initial)
|
||||
}
|
||||
waitForWatchCalls(t, runtimeStore, 1)
|
||||
|
||||
runtimeStore.setCurrent(domain.RuntimeConfig{Items: map[string]string{"feature": "latest"}, Revision: 8})
|
||||
first <- runtimepkg.WatchResult{Err: errors.Join(runtimepkg.ErrRevisionCompacted, errors.New("compacted")), CompactRevision: 7}
|
||||
resync := receiveEvent(t, events)
|
||||
if resync.Type != "FULL_SYNC" || resync.Revision != 8 || resync.Items["feature"] != "latest" {
|
||||
t.Fatalf("unexpected compacted resync: %#v", resync)
|
||||
}
|
||||
|
||||
waitForWatchCalls(t, runtimeStore, 2)
|
||||
if calls := runtimeStore.watchRevisions(); calls[1] != 9 {
|
||||
t.Fatalf("watch did not resume from snapshot+1: %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubRefreshesMatchingScopeWithoutRevisionChange(t *testing.T) {
|
||||
@@ -97,7 +129,7 @@ func TestHubRetriesInitialSnapshotBeforeWatching(t *testing.T) {
|
||||
defer unsubscribe()
|
||||
|
||||
event := receiveEvent(t, events)
|
||||
if event.Revision != 9 || event.Items["feature"] != "ready" {
|
||||
if event.Type != "FULL_SYNC" || event.Revision != 9 || event.Items["feature"] != "ready" {
|
||||
t.Fatalf("unexpected snapshot after retry: %#v", event)
|
||||
}
|
||||
waitForWatchCalls(t, runtimeStore, 1)
|
||||
|
||||
857
pkg/proto/v1/config.pb.go
Normal file
857
pkg/proto/v1/config.pb.go
Normal file
@@ -0,0 +1,857 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v4.25.1
|
||||
// source: configcenter/v1/config.proto
|
||||
|
||||
package configcenterv1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ConfigEvent_EventType int32
|
||||
|
||||
const (
|
||||
ConfigEvent_FULL_SYNC ConfigEvent_EventType = 0
|
||||
ConfigEvent_UPDATED ConfigEvent_EventType = 1
|
||||
)
|
||||
|
||||
// Enum value maps for ConfigEvent_EventType.
|
||||
var (
|
||||
ConfigEvent_EventType_name = map[int32]string{
|
||||
0: "FULL_SYNC",
|
||||
1: "UPDATED",
|
||||
}
|
||||
ConfigEvent_EventType_value = map[string]int32{
|
||||
"FULL_SYNC": 0,
|
||||
"UPDATED": 1,
|
||||
}
|
||||
)
|
||||
|
||||
func (x ConfigEvent_EventType) Enum() *ConfigEvent_EventType {
|
||||
p := new(ConfigEvent_EventType)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x ConfigEvent_EventType) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (ConfigEvent_EventType) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_configcenter_v1_config_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (ConfigEvent_EventType) Type() protoreflect.EnumType {
|
||||
return &file_configcenter_v1_config_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x ConfigEvent_EventType) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConfigEvent_EventType.Descriptor instead.
|
||||
func (ConfigEvent_EventType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{4, 0}
|
||||
}
|
||||
|
||||
type ConfigItem struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ConfigItem) Reset() {
|
||||
*x = ConfigItem{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ConfigItem) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ConfigItem) ProtoMessage() {}
|
||||
|
||||
func (x *ConfigItem) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[0]
|
||||
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 ConfigItem.ProtoReflect.Descriptor instead.
|
||||
func (*ConfigItem) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ConfigItem) GetKey() string {
|
||||
if x != nil {
|
||||
return x.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ConfigItem) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetConfigRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Env string `protobuf:"bytes,1,opt,name=env,proto3" json:"env,omitempty"`
|
||||
App string `protobuf:"bytes,2,opt,name=app,proto3" json:"app,omitempty"`
|
||||
Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Ip string `protobuf:"bytes,4,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||
Instance string `protobuf:"bytes,5,opt,name=instance,proto3" json:"instance,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetConfigRequest) Reset() {
|
||||
*x = GetConfigRequest{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetConfigRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetConfigRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetConfigRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[1]
|
||||
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 GetConfigRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetConfigRequest) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *GetConfigRequest) GetEnv() string {
|
||||
if x != nil {
|
||||
return x.Env
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetConfigRequest) GetApp() string {
|
||||
if x != nil {
|
||||
return x.App
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetConfigRequest) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetConfigRequest) GetIp() string {
|
||||
if x != nil {
|
||||
return x.Ip
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetConfigRequest) GetInstance() string {
|
||||
if x != nil {
|
||||
return x.Instance
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetConfigResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Items []*ConfigItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
|
||||
Revision int64 `protobuf:"varint,2,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
ReleaseVersion int64 `protobuf:"varint,3,opt,name=release_version,json=releaseVersion,proto3" json:"release_version,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetConfigResponse) Reset() {
|
||||
*x = GetConfigResponse{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetConfigResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetConfigResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetConfigResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[2]
|
||||
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 GetConfigResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetConfigResponse) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *GetConfigResponse) GetItems() []*ConfigItem {
|
||||
if x != nil {
|
||||
return x.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetConfigResponse) GetRevision() int64 {
|
||||
if x != nil {
|
||||
return x.Revision
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GetConfigResponse) GetReleaseVersion() int64 {
|
||||
if x != nil {
|
||||
return x.ReleaseVersion
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type WatchConfigRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Env string `protobuf:"bytes,1,opt,name=env,proto3" json:"env,omitempty"`
|
||||
App string `protobuf:"bytes,2,opt,name=app,proto3" json:"app,omitempty"`
|
||||
Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
StartRevision int64 `protobuf:"varint,4,opt,name=start_revision,json=startRevision,proto3" json:"start_revision,omitempty"`
|
||||
Ip string `protobuf:"bytes,5,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||
Instance string `protobuf:"bytes,6,opt,name=instance,proto3" json:"instance,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) Reset() {
|
||||
*x = WatchConfigRequest{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*WatchConfigRequest) ProtoMessage() {}
|
||||
|
||||
func (x *WatchConfigRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[3]
|
||||
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 WatchConfigRequest.ProtoReflect.Descriptor instead.
|
||||
func (*WatchConfigRequest) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) GetEnv() string {
|
||||
if x != nil {
|
||||
return x.Env
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) GetApp() string {
|
||||
if x != nil {
|
||||
return x.App
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) GetStartRevision() int64 {
|
||||
if x != nil {
|
||||
return x.StartRevision
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) GetIp() string {
|
||||
if x != nil {
|
||||
return x.Ip
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WatchConfigRequest) GetInstance() string {
|
||||
if x != nil {
|
||||
return x.Instance
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ConfigEvent struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type ConfigEvent_EventType `protobuf:"varint,1,opt,name=type,proto3,enum=configcenter.v1.ConfigEvent_EventType" json:"type,omitempty"`
|
||||
Items []*ConfigItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"`
|
||||
Revision int64 `protobuf:"varint,3,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ConfigEvent) Reset() {
|
||||
*x = ConfigEvent{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ConfigEvent) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ConfigEvent) ProtoMessage() {}
|
||||
|
||||
func (x *ConfigEvent) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[4]
|
||||
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 ConfigEvent.ProtoReflect.Descriptor instead.
|
||||
func (*ConfigEvent) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *ConfigEvent) GetType() ConfigEvent_EventType {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ConfigEvent_FULL_SYNC
|
||||
}
|
||||
|
||||
func (x *ConfigEvent) GetItems() []*ConfigItem {
|
||||
if x != nil {
|
||||
return x.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ConfigEvent) GetRevision() int64 {
|
||||
if x != nil {
|
||||
return x.Revision
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type PublishRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EnvId int64 `protobuf:"varint,1,opt,name=env_id,json=envId,proto3" json:"env_id,omitempty"`
|
||||
AppId int64 `protobuf:"varint,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"`
|
||||
NamespaceId int64 `protobuf:"varint,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"`
|
||||
Comment string `protobuf:"bytes,4,opt,name=comment,proto3" json:"comment,omitempty"`
|
||||
Operator string `protobuf:"bytes,5,opt,name=operator,proto3" json:"operator,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PublishRequest) Reset() {
|
||||
*x = PublishRequest{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PublishRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PublishRequest) ProtoMessage() {}
|
||||
|
||||
func (x *PublishRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_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 PublishRequest.ProtoReflect.Descriptor instead.
|
||||
func (*PublishRequest) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *PublishRequest) GetEnvId() int64 {
|
||||
if x != nil {
|
||||
return x.EnvId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PublishRequest) GetAppId() int64 {
|
||||
if x != nil {
|
||||
return x.AppId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PublishRequest) GetNamespaceId() int64 {
|
||||
if x != nil {
|
||||
return x.NamespaceId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PublishRequest) GetComment() string {
|
||||
if x != nil {
|
||||
return x.Comment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PublishRequest) GetOperator() string {
|
||||
if x != nil {
|
||||
return x.Operator
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PublishResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ReleaseId int64 `protobuf:"varint,1,opt,name=release_id,json=releaseId,proto3" json:"release_id,omitempty"`
|
||||
ReleaseVersion int64 `protobuf:"varint,2,opt,name=release_version,json=releaseVersion,proto3" json:"release_version,omitempty"`
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PublishResponse) Reset() {
|
||||
*x = PublishResponse{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PublishResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PublishResponse) ProtoMessage() {}
|
||||
|
||||
func (x *PublishResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[6]
|
||||
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 PublishResponse.ProtoReflect.Descriptor instead.
|
||||
func (*PublishResponse) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *PublishResponse) GetReleaseId() int64 {
|
||||
if x != nil {
|
||||
return x.ReleaseId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PublishResponse) GetReleaseVersion() int64 {
|
||||
if x != nil {
|
||||
return x.ReleaseVersion
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PublishResponse) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RollbackRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EnvId int64 `protobuf:"varint,1,opt,name=env_id,json=envId,proto3" json:"env_id,omitempty"`
|
||||
AppId int64 `protobuf:"varint,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"`
|
||||
NamespaceId int64 `protobuf:"varint,3,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"`
|
||||
TargetVersion int64 `protobuf:"varint,4,opt,name=target_version,json=targetVersion,proto3" json:"target_version,omitempty"`
|
||||
Operator string `protobuf:"bytes,5,opt,name=operator,proto3" json:"operator,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RollbackRequest) Reset() {
|
||||
*x = RollbackRequest{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RollbackRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RollbackRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RollbackRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[7]
|
||||
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 RollbackRequest.ProtoReflect.Descriptor instead.
|
||||
func (*RollbackRequest) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *RollbackRequest) GetEnvId() int64 {
|
||||
if x != nil {
|
||||
return x.EnvId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RollbackRequest) GetAppId() int64 {
|
||||
if x != nil {
|
||||
return x.AppId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RollbackRequest) GetNamespaceId() int64 {
|
||||
if x != nil {
|
||||
return x.NamespaceId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RollbackRequest) GetTargetVersion() int64 {
|
||||
if x != nil {
|
||||
return x.TargetVersion
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RollbackRequest) GetOperator() string {
|
||||
if x != nil {
|
||||
return x.Operator
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RollbackResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ReleaseId int64 `protobuf:"varint,1,opt,name=release_id,json=releaseId,proto3" json:"release_id,omitempty"`
|
||||
NewReleaseVersion int64 `protobuf:"varint,2,opt,name=new_release_version,json=newReleaseVersion,proto3" json:"new_release_version,omitempty"`
|
||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RollbackResponse) Reset() {
|
||||
*x = RollbackResponse{}
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RollbackResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RollbackResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RollbackResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_configcenter_v1_config_proto_msgTypes[8]
|
||||
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 RollbackResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RollbackResponse) Descriptor() ([]byte, []int) {
|
||||
return file_configcenter_v1_config_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *RollbackResponse) GetReleaseId() int64 {
|
||||
if x != nil {
|
||||
return x.ReleaseId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RollbackResponse) GetNewReleaseVersion() int64 {
|
||||
if x != nil {
|
||||
return x.NewReleaseVersion
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RollbackResponse) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_configcenter_v1_config_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_configcenter_v1_config_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x1c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2f, 0x76,
|
||||
0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f,
|
||||
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22,
|
||||
0x34, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a,
|
||||
0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
|
||||
0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x80, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e,
|
||||
0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e,
|
||||
0x76, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x10, 0x0a, 0x03,
|
||||
0x61, 0x70, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x70, 0x70, 0x12, 0x1c,
|
||||
0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02,
|
||||
0x69, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08,
|
||||
0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||
0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31,
|
||||
0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e,
|
||||
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d,
|
||||
0x73, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a,
|
||||
0x0f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x56,
|
||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xa9, 0x01, 0x0a, 0x12, 0x57, 0x61, 0x74, 0x63, 0x68,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a,
|
||||
0x03, 0x65, 0x6e, 0x76, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12,
|
||||
0x10, 0x0a, 0x03, 0x61, 0x70, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x70,
|
||||
0x70, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12,
|
||||
0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65,
|
||||
0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x05, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e,
|
||||
0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e,
|
||||
0x63, 0x65, 0x22, 0xc1, 0x01, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x76, 0x65,
|
||||
0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
|
||||
0x32, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x45,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x31,
|
||||
0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e,
|
||||
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d,
|
||||
0x73, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x27, 0x0a,
|
||||
0x09, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x55,
|
||||
0x4c, 0x4c, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44,
|
||||
0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x22, 0x97, 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x62, 0x6c, 0x69,
|
||||
0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x65, 0x6e, 0x76,
|
||||
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x65, 0x6e, 0x76, 0x49, 0x64,
|
||||
0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x05, 0x61, 0x70, 0x70, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72,
|
||||
0x22, 0x71, 0x0a, 0x0f, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65,
|
||||
0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x76, 0x65,
|
||||
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x6c,
|
||||
0x65, 0x61, 0x73, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x22, 0xa5, 0x01, 0x0a, 0x0f, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x65, 0x6e, 0x76, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x65, 0x6e, 0x76, 0x49, 0x64, 0x12, 0x15,
|
||||
0x0a, 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
|
||||
0x61, 0x70, 0x70, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
|
||||
0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
|
||||
0x65, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12,
|
||||
0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x79, 0x0a, 0x10, 0x52,
|
||||
0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x2e,
|
||||
0x0a, 0x13, 0x6e, 0x65, 0x77, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x76, 0x65,
|
||||
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x6e, 0x65, 0x77,
|
||||
0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16,
|
||||
0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
||||
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x32, 0xb7, 0x01, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69,
|
||||
0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65,
|
||||
0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
|
||||
0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69,
|
||||
0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0b,
|
||||
0x57, 0x61, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x2e, 0x63, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x61,
|
||||
0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01,
|
||||
0x32, 0xb9, 0x01, 0x0a, 0x0c, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x65, 0x12, 0x52, 0x0a, 0x0d, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63,
|
||||
0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61,
|
||||
0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x63, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x6c,
|
||||
0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3e, 0x5a, 0x3c,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x6f, 0x6e, 0x67, 0x70,
|
||||
0x65, 0x6e, 0x67, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72,
|
||||
0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
file_configcenter_v1_config_proto_rawDescOnce sync.Once
|
||||
file_configcenter_v1_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_configcenter_v1_config_proto_rawDescGZIP() []byte {
|
||||
file_configcenter_v1_config_proto_rawDescOnce.Do(func() {
|
||||
file_configcenter_v1_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_configcenter_v1_config_proto_rawDesc), len(file_configcenter_v1_config_proto_rawDesc)))
|
||||
})
|
||||
return file_configcenter_v1_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_configcenter_v1_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_configcenter_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_configcenter_v1_config_proto_goTypes = []any{
|
||||
(ConfigEvent_EventType)(0), // 0: configcenter.v1.ConfigEvent.EventType
|
||||
(*ConfigItem)(nil), // 1: configcenter.v1.ConfigItem
|
||||
(*GetConfigRequest)(nil), // 2: configcenter.v1.GetConfigRequest
|
||||
(*GetConfigResponse)(nil), // 3: configcenter.v1.GetConfigResponse
|
||||
(*WatchConfigRequest)(nil), // 4: configcenter.v1.WatchConfigRequest
|
||||
(*ConfigEvent)(nil), // 5: configcenter.v1.ConfigEvent
|
||||
(*PublishRequest)(nil), // 6: configcenter.v1.PublishRequest
|
||||
(*PublishResponse)(nil), // 7: configcenter.v1.PublishResponse
|
||||
(*RollbackRequest)(nil), // 8: configcenter.v1.RollbackRequest
|
||||
(*RollbackResponse)(nil), // 9: configcenter.v1.RollbackResponse
|
||||
}
|
||||
var file_configcenter_v1_config_proto_depIdxs = []int32{
|
||||
1, // 0: configcenter.v1.GetConfigResponse.items:type_name -> configcenter.v1.ConfigItem
|
||||
0, // 1: configcenter.v1.ConfigEvent.type:type_name -> configcenter.v1.ConfigEvent.EventType
|
||||
1, // 2: configcenter.v1.ConfigEvent.items:type_name -> configcenter.v1.ConfigItem
|
||||
2, // 3: configcenter.v1.ConfigService.GetConfig:input_type -> configcenter.v1.GetConfigRequest
|
||||
4, // 4: configcenter.v1.ConfigService.WatchConfig:input_type -> configcenter.v1.WatchConfigRequest
|
||||
6, // 5: configcenter.v1.AdminService.PublishConfig:input_type -> configcenter.v1.PublishRequest
|
||||
8, // 6: configcenter.v1.AdminService.RollbackConfig:input_type -> configcenter.v1.RollbackRequest
|
||||
3, // 7: configcenter.v1.ConfigService.GetConfig:output_type -> configcenter.v1.GetConfigResponse
|
||||
5, // 8: configcenter.v1.ConfigService.WatchConfig:output_type -> configcenter.v1.ConfigEvent
|
||||
7, // 9: configcenter.v1.AdminService.PublishConfig:output_type -> configcenter.v1.PublishResponse
|
||||
9, // 10: configcenter.v1.AdminService.RollbackConfig:output_type -> configcenter.v1.RollbackResponse
|
||||
7, // [7:11] is the sub-list for method output_type
|
||||
3, // [3:7] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_configcenter_v1_config_proto_init() }
|
||||
func file_configcenter_v1_config_proto_init() {
|
||||
if File_configcenter_v1_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_configcenter_v1_config_proto_rawDesc), len(file_configcenter_v1_config_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 2,
|
||||
},
|
||||
GoTypes: file_configcenter_v1_config_proto_goTypes,
|
||||
DependencyIndexes: file_configcenter_v1_config_proto_depIdxs,
|
||||
EnumInfos: file_configcenter_v1_config_proto_enumTypes,
|
||||
MessageInfos: file_configcenter_v1_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_configcenter_v1_config_proto = out.File
|
||||
file_configcenter_v1_config_proto_goTypes = nil
|
||||
file_configcenter_v1_config_proto_depIdxs = nil
|
||||
}
|
||||
303
pkg/proto/v1/config_grpc.pb.go
Normal file
303
pkg/proto/v1/config_grpc.pb.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v4.25.1
|
||||
// source: configcenter/v1/config.proto
|
||||
|
||||
package configcenterv1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
ConfigService_GetConfig_FullMethodName = "/configcenter.v1.ConfigService/GetConfig"
|
||||
ConfigService_WatchConfig_FullMethodName = "/configcenter.v1.ConfigService/WatchConfig"
|
||||
)
|
||||
|
||||
// ConfigServiceClient is the client API for ConfigService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ConfigServiceClient interface {
|
||||
GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
|
||||
WatchConfig(ctx context.Context, in *WatchConfigRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConfigEvent], error)
|
||||
}
|
||||
|
||||
type configServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewConfigServiceClient(cc grpc.ClientConnInterface) ConfigServiceClient {
|
||||
return &configServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *configServiceClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetConfigResponse)
|
||||
err := c.cc.Invoke(ctx, ConfigService_GetConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *configServiceClient) WatchConfig(ctx context.Context, in *WatchConfigRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConfigEvent], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &ConfigService_ServiceDesc.Streams[0], ConfigService_WatchConfig_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[WatchConfigRequest, ConfigEvent]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type ConfigService_WatchConfigClient = grpc.ServerStreamingClient[ConfigEvent]
|
||||
|
||||
// ConfigServiceServer is the server API for ConfigService service.
|
||||
// All implementations must embed UnimplementedConfigServiceServer
|
||||
// for forward compatibility.
|
||||
type ConfigServiceServer interface {
|
||||
GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
|
||||
WatchConfig(*WatchConfigRequest, grpc.ServerStreamingServer[ConfigEvent]) error
|
||||
mustEmbedUnimplementedConfigServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedConfigServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedConfigServiceServer struct{}
|
||||
|
||||
func (UnimplementedConfigServiceServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented")
|
||||
}
|
||||
func (UnimplementedConfigServiceServer) WatchConfig(*WatchConfigRequest, grpc.ServerStreamingServer[ConfigEvent]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method WatchConfig not implemented")
|
||||
}
|
||||
func (UnimplementedConfigServiceServer) mustEmbedUnimplementedConfigServiceServer() {}
|
||||
func (UnimplementedConfigServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeConfigServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ConfigServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeConfigServiceServer interface {
|
||||
mustEmbedUnimplementedConfigServiceServer()
|
||||
}
|
||||
|
||||
func RegisterConfigServiceServer(s grpc.ServiceRegistrar, srv ConfigServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedConfigServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&ConfigService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ConfigService_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ConfigServiceServer).GetConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ConfigService_GetConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ConfigServiceServer).GetConfig(ctx, req.(*GetConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ConfigService_WatchConfig_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(WatchConfigRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(ConfigServiceServer).WatchConfig(m, &grpc.GenericServerStream[WatchConfigRequest, ConfigEvent]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type ConfigService_WatchConfigServer = grpc.ServerStreamingServer[ConfigEvent]
|
||||
|
||||
// ConfigService_ServiceDesc is the grpc.ServiceDesc for ConfigService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ConfigService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "configcenter.v1.ConfigService",
|
||||
HandlerType: (*ConfigServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetConfig",
|
||||
Handler: _ConfigService_GetConfig_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "WatchConfig",
|
||||
Handler: _ConfigService_WatchConfig_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "configcenter/v1/config.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
AdminService_PublishConfig_FullMethodName = "/configcenter.v1.AdminService/PublishConfig"
|
||||
AdminService_RollbackConfig_FullMethodName = "/configcenter.v1.AdminService/RollbackConfig"
|
||||
)
|
||||
|
||||
// AdminServiceClient is the client API for AdminService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type AdminServiceClient interface {
|
||||
PublishConfig(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error)
|
||||
RollbackConfig(ctx context.Context, in *RollbackRequest, opts ...grpc.CallOption) (*RollbackResponse, error)
|
||||
}
|
||||
|
||||
type adminServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAdminServiceClient(cc grpc.ClientConnInterface) AdminServiceClient {
|
||||
return &adminServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *adminServiceClient) PublishConfig(ctx context.Context, in *PublishRequest, opts ...grpc.CallOption) (*PublishResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PublishResponse)
|
||||
err := c.cc.Invoke(ctx, AdminService_PublishConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminServiceClient) RollbackConfig(ctx context.Context, in *RollbackRequest, opts ...grpc.CallOption) (*RollbackResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RollbackResponse)
|
||||
err := c.cc.Invoke(ctx, AdminService_RollbackConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminServiceServer is the server API for AdminService service.
|
||||
// All implementations must embed UnimplementedAdminServiceServer
|
||||
// for forward compatibility.
|
||||
type AdminServiceServer interface {
|
||||
PublishConfig(context.Context, *PublishRequest) (*PublishResponse, error)
|
||||
RollbackConfig(context.Context, *RollbackRequest) (*RollbackResponse, error)
|
||||
mustEmbedUnimplementedAdminServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAdminServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedAdminServiceServer struct{}
|
||||
|
||||
func (UnimplementedAdminServiceServer) PublishConfig(context.Context, *PublishRequest) (*PublishResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PublishConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminServiceServer) RollbackConfig(context.Context, *RollbackRequest) (*RollbackResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RollbackConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminServiceServer) mustEmbedUnimplementedAdminServiceServer() {}
|
||||
func (UnimplementedAdminServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAdminServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AdminServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAdminServiceServer interface {
|
||||
mustEmbedUnimplementedAdminServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAdminServiceServer(s grpc.ServiceRegistrar, srv AdminServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAdminServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&AdminService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AdminService_PublishConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PublishRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminServiceServer).PublishConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminService_PublishConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminServiceServer).PublishConfig(ctx, req.(*PublishRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminService_RollbackConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RollbackRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminServiceServer).RollbackConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminService_RollbackConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminServiceServer).RollbackConfig(ctx, req.(*RollbackRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AdminService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "configcenter.v1.AdminService",
|
||||
HandlerType: (*AdminServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "PublishConfig",
|
||||
Handler: _AdminService_PublishConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RollbackConfig",
|
||||
Handler: _AdminService_RollbackConfig_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "configcenter/v1/config.proto",
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package configsdk
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -17,6 +18,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
configcenterv1 "github.com/longpeng/configcenter/pkg/proto/v1"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
@@ -30,6 +37,17 @@ type Options struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
type GRPCOptions struct {
|
||||
Target string
|
||||
Env string
|
||||
App string
|
||||
Token string
|
||||
IP string
|
||||
Instance string
|
||||
CacheFile string
|
||||
DialOptions []grpc.DialOption
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
env string
|
||||
@@ -39,6 +57,8 @@ type Client struct {
|
||||
instance string
|
||||
cacheFile string
|
||||
http *http.Client
|
||||
grpcConn *grpc.ClientConn
|
||||
grpc configcenterv1.ConfigServiceClient
|
||||
|
||||
mu sync.RWMutex
|
||||
cache map[string]map[string]string
|
||||
@@ -94,6 +114,46 @@ func New(options Options) (*Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewGRPC(options GRPCOptions) (*Client, error) {
|
||||
if strings.TrimSpace(options.Target) == "" || strings.TrimSpace(options.Env) == "" || strings.TrimSpace(options.App) == "" {
|
||||
return nil, errors.New("gRPC target, environment and application are required")
|
||||
}
|
||||
dialOptions := append([]grpc.DialOption(nil), options.DialOptions...)
|
||||
if len(dialOptions) == 0 {
|
||||
dialOptions = append(dialOptions, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})))
|
||||
}
|
||||
connection, err := grpc.NewClient(strings.TrimSpace(options.Target), dialOptions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect config center gRPC: %w", err)
|
||||
}
|
||||
client := &Client{
|
||||
env: strings.TrimSpace(options.Env),
|
||||
app: strings.TrimSpace(options.App),
|
||||
token: strings.TrimSpace(options.Token),
|
||||
ip: strings.TrimSpace(options.IP),
|
||||
instance: strings.TrimSpace(options.Instance),
|
||||
cacheFile: options.CacheFile,
|
||||
grpcConn: connection,
|
||||
grpc: configcenterv1.NewConfigServiceClient(connection),
|
||||
cache: make(map[string]map[string]string),
|
||||
revisions: make(map[string]int64),
|
||||
}
|
||||
if options.CacheFile != "" {
|
||||
if err := client.loadDiskCache(); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
_ = connection.Close()
|
||||
return nil, fmt.Errorf("load config cache: %w", err)
|
||||
}
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.grpcConn != nil {
|
||||
return c.grpcConn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetString(namespace, key, fallback string) string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
@@ -130,6 +190,9 @@ func (c *Client) Snapshot(namespace string) map[string]string {
|
||||
}
|
||||
|
||||
func (c *Client) Load(ctx context.Context, namespace string) error {
|
||||
if c.grpc != nil {
|
||||
return c.loadGRPC(ctx, namespace)
|
||||
}
|
||||
endpoint := c.endpoint("/v1/config", namespace)
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
@@ -181,6 +244,9 @@ func (c *Client) WatchAndSync(ctx context.Context, namespace string) error {
|
||||
}
|
||||
|
||||
func (c *Client) watchOnce(ctx context.Context, namespace string) error {
|
||||
if c.grpc != nil {
|
||||
return c.watchGRPCOnce(ctx, namespace)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint("/v1/watch", namespace), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -226,6 +292,56 @@ func (c *Client) watchOnce(ctx context.Context, namespace string) error {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func (c *Client) loadGRPC(ctx context.Context, namespace string) error {
|
||||
response, err := c.grpc.GetConfig(c.grpcContext(ctx), &configcenterv1.GetConfigRequest{
|
||||
Env: c.env, App: c.app, Namespace: namespace, Ip: c.ip, Instance: c.instance,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.apply(namespace, protoItemMap(response.GetItems()), response.GetRevision())
|
||||
}
|
||||
|
||||
func (c *Client) watchGRPCOnce(ctx context.Context, namespace string) error {
|
||||
c.mu.RLock()
|
||||
lastRevision := c.revisions[namespace]
|
||||
c.mu.RUnlock()
|
||||
startRevision := int64(0)
|
||||
if lastRevision > 0 {
|
||||
startRevision = lastRevision + 1
|
||||
}
|
||||
stream, err := c.grpc.WatchConfig(c.grpcContext(ctx), &configcenterv1.WatchConfigRequest{
|
||||
Env: c.env, App: c.app, Namespace: namespace, StartRevision: startRevision, Ip: c.ip, Instance: c.instance,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
event, err := stream.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.apply(namespace, protoItemMap(event.GetItems()), event.GetRevision()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) grpcContext(ctx context.Context) context.Context {
|
||||
if c.token == "" {
|
||||
return ctx
|
||||
}
|
||||
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+c.token)
|
||||
}
|
||||
|
||||
func protoItemMap(items []*configcenterv1.ConfigItem) map[string]string {
|
||||
result := make(map[string]string, len(items))
|
||||
for _, item := range items {
|
||||
result[item.GetKey()] = item.GetValue()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Client) endpoint(path, namespace string) string {
|
||||
query := url.Values{"env": {c.env}, "app": {c.app}, "namespace": {namespace}}
|
||||
if c.ip != "" {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Thread-safe Config Center client using only Python's standard library."""
|
||||
"""Thread-safe Config Center client with HTTP/SSE and optional gRPC transports."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -24,20 +24,38 @@ class ConfigClient:
|
||||
token: str | None = None,
|
||||
ip: str | None = None,
|
||||
instance: str | None = None,
|
||||
transport: str = "http",
|
||||
grpc_secure: bool = True,
|
||||
):
|
||||
if not base_url or not env or not app:
|
||||
raise ValueError("base_url, env and app are required")
|
||||
if transport not in {"http", "grpc"}:
|
||||
raise ValueError("transport must be 'http' or 'grpc'")
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._env = env
|
||||
self._app = app
|
||||
self._token = token or ""
|
||||
self._ip = ip or ""
|
||||
self._instance = instance or ""
|
||||
self._transport = transport
|
||||
self._grpc_secure = grpc_secure
|
||||
self._grpc_channel: Any = None
|
||||
self._grpc_stub: Any = None
|
||||
self._grpc_pb2: Any = None
|
||||
self._cache_file = Path(cache_file) if cache_file else None
|
||||
self._cache: dict[str, dict[str, str]] = {}
|
||||
self._revisions: dict[str, int] = {}
|
||||
self._lock = threading.RLock()
|
||||
self._load_disk_cache()
|
||||
if self._transport == "grpc":
|
||||
self._init_grpc()
|
||||
|
||||
def close(self) -> None:
|
||||
channel = self._grpc_channel
|
||||
if channel is not None:
|
||||
channel.close()
|
||||
self._grpc_channel = None
|
||||
self._grpc_stub = None
|
||||
|
||||
def get(self, namespace: str, key: str, default: Any = None) -> Any:
|
||||
with self._lock:
|
||||
@@ -48,6 +66,9 @@ class ConfigClient:
|
||||
return dict(self._cache.get(namespace, {}))
|
||||
|
||||
def load(self, namespace: str, timeout: float = 10.0) -> None:
|
||||
if self._transport == "grpc":
|
||||
self._load_grpc(namespace, timeout)
|
||||
return
|
||||
with urllib.request.urlopen(self._request("/v1/config", namespace), timeout=timeout) as response:
|
||||
payload = json.load(response)["data"]
|
||||
self._apply(namespace, payload.get("items", {}), int(payload.get("revision", 0)))
|
||||
@@ -61,24 +82,88 @@ class ConfigClient:
|
||||
backoff = 1.0
|
||||
while True:
|
||||
try:
|
||||
request = self._request("/v1/watch", namespace, {"Accept": "text/event-stream"})
|
||||
with urllib.request.urlopen(request, timeout=None) as response:
|
||||
backoff = 1.0
|
||||
data_lines: list[str] = []
|
||||
for raw_line in response:
|
||||
line = raw_line.decode("utf-8").rstrip("\r\n")
|
||||
if not line:
|
||||
if data_lines:
|
||||
event = json.loads("\n".join(data_lines))
|
||||
self._apply(namespace, event.get("items", {}), int(event.get("revision", 0)))
|
||||
data_lines.clear()
|
||||
continue
|
||||
if line.startswith("data:"):
|
||||
data_lines.append(line[5:].strip())
|
||||
if self._transport == "grpc":
|
||||
self._watch_grpc_once(namespace)
|
||||
else:
|
||||
self._watch_http_once(namespace)
|
||||
backoff = 1.0
|
||||
except Exception:
|
||||
time.sleep(backoff)
|
||||
backoff = min(backoff * 2, 30.0)
|
||||
|
||||
def _watch_http_once(self, namespace: str) -> None:
|
||||
request = self._request("/v1/watch", namespace, {"Accept": "text/event-stream"})
|
||||
with urllib.request.urlopen(request, timeout=None) as response:
|
||||
data_lines: list[str] = []
|
||||
for raw_line in response:
|
||||
line = raw_line.decode("utf-8").rstrip("\r\n")
|
||||
if not line:
|
||||
if data_lines:
|
||||
event = json.loads("\n".join(data_lines))
|
||||
self._apply(namespace, event.get("items", {}), int(event.get("revision", 0)))
|
||||
data_lines.clear()
|
||||
continue
|
||||
if line.startswith("data:"):
|
||||
data_lines.append(line[5:].strip())
|
||||
|
||||
def _init_grpc(self) -> None:
|
||||
try:
|
||||
import grpc
|
||||
from .v1 import config_pb2, config_pb2_grpc
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"gRPC transport requires the 'grpc' extra: pip install 'configcenter-client[grpc]'"
|
||||
) from exc
|
||||
|
||||
if self._grpc_secure:
|
||||
credentials = grpc.ssl_channel_credentials()
|
||||
channel = grpc.secure_channel(self._base_url, credentials)
|
||||
else:
|
||||
channel = grpc.insecure_channel(self._base_url)
|
||||
self._grpc_channel = channel
|
||||
self._grpc_stub = config_pb2_grpc.ConfigServiceStub(channel)
|
||||
self._grpc_pb2 = config_pb2
|
||||
|
||||
def _load_grpc(self, namespace: str, timeout: float) -> None:
|
||||
request = self._grpc_pb2.GetConfigRequest(
|
||||
env=self._env,
|
||||
app=self._app,
|
||||
namespace=namespace,
|
||||
ip=self._ip,
|
||||
instance=self._instance,
|
||||
)
|
||||
response = self._grpc_stub.GetConfig(request, timeout=timeout, metadata=self._grpc_metadata())
|
||||
self._apply(
|
||||
namespace,
|
||||
{item.key: item.value for item in response.items},
|
||||
int(response.revision),
|
||||
)
|
||||
|
||||
def _watch_grpc_once(self, namespace: str) -> None:
|
||||
with self._lock:
|
||||
last_revision = int(self._revisions.get(namespace, 0))
|
||||
start_revision = last_revision + 1 if last_revision > 0 else 0
|
||||
request = self._grpc_pb2.WatchConfigRequest(
|
||||
env=self._env,
|
||||
app=self._app,
|
||||
namespace=namespace,
|
||||
start_revision=start_revision,
|
||||
ip=self._ip,
|
||||
instance=self._instance,
|
||||
)
|
||||
stream = self._grpc_stub.WatchConfig(request, metadata=self._grpc_metadata())
|
||||
for event in stream:
|
||||
self._apply(
|
||||
namespace,
|
||||
{item.key: item.value for item in event.items},
|
||||
int(event.revision),
|
||||
)
|
||||
|
||||
def _grpc_metadata(self) -> tuple[tuple[str, str], ...]:
|
||||
if not self._token:
|
||||
return ()
|
||||
return (("authorization", f"Bearer {self._token}"),)
|
||||
|
||||
def _url(self, path: str, namespace: str) -> str:
|
||||
values = {"env": self._env, "app": self._app, "namespace": namespace}
|
||||
if self._ip:
|
||||
@@ -109,7 +194,7 @@ class ConfigClient:
|
||||
return
|
||||
payload = json.loads(self._cache_file.read_text(encoding="utf-8"))
|
||||
self._cache = payload.get("namespaces", {})
|
||||
self._revisions = payload.get("revisions", {})
|
||||
self._revisions = {key: int(value) for key, value in payload.get("revisions", {}).items()}
|
||||
|
||||
def _save_disk_cache(self) -> None:
|
||||
if not self._cache_file:
|
||||
|
||||
0
sdk/python/configcenter/v1/__init__.py
Normal file
0
sdk/python/configcenter/v1/__init__.py
Normal file
59
sdk/python/configcenter/v1/config_pb2.py
Normal file
59
sdk/python/configcenter/v1/config_pb2.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: configcenter/v1/config.proto
|
||||
# Protobuf Python Version: 5.29.0
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
5,
|
||||
29,
|
||||
0,
|
||||
'',
|
||||
'configcenter/v1/config.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x63onfigcenter/v1/config.proto\x12\x0f\x63onfigcenter.v1\"(\n\nConfigItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"]\n\x10GetConfigRequest\x12\x0b\n\x03\x65nv\x18\x01 \x01(\t\x12\x0b\n\x03\x61pp\x18\x02 \x01(\t\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\n\n\x02ip\x18\x04 \x01(\t\x12\x10\n\x08instance\x18\x05 \x01(\t\"j\n\x11GetConfigResponse\x12*\n\x05items\x18\x01 \x03(\x0b\x32\x1b.configcenter.v1.ConfigItem\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x17\n\x0frelease_version\x18\x03 \x01(\x03\"w\n\x12WatchConfigRequest\x12\x0b\n\x03\x65nv\x18\x01 \x01(\t\x12\x0b\n\x03\x61pp\x18\x02 \x01(\t\x12\x11\n\tnamespace\x18\x03 \x01(\t\x12\x16\n\x0estart_revision\x18\x04 \x01(\x03\x12\n\n\x02ip\x18\x05 \x01(\t\x12\x10\n\x08instance\x18\x06 \x01(\t\"\xaa\x01\n\x0b\x43onfigEvent\x12\x34\n\x04type\x18\x01 \x01(\x0e\x32&.configcenter.v1.ConfigEvent.EventType\x12*\n\x05items\x18\x02 \x03(\x0b\x32\x1b.configcenter.v1.ConfigItem\x12\x10\n\x08revision\x18\x03 \x01(\x03\"\'\n\tEventType\x12\r\n\tFULL_SYNC\x10\x00\x12\x0b\n\x07UPDATED\x10\x01\"i\n\x0ePublishRequest\x12\x0e\n\x06\x65nv_id\x18\x01 \x01(\x03\x12\x0e\n\x06\x61pp_id\x18\x02 \x01(\x03\x12\x14\n\x0cnamespace_id\x18\x03 \x01(\x03\x12\x0f\n\x07\x63omment\x18\x04 \x01(\t\x12\x10\n\x08operator\x18\x05 \x01(\t\"N\n\x0fPublishResponse\x12\x12\n\nrelease_id\x18\x01 \x01(\x03\x12\x17\n\x0frelease_version\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t\"q\n\x0fRollbackRequest\x12\x0e\n\x06\x65nv_id\x18\x01 \x01(\x03\x12\x0e\n\x06\x61pp_id\x18\x02 \x01(\x03\x12\x14\n\x0cnamespace_id\x18\x03 \x01(\x03\x12\x16\n\x0etarget_version\x18\x04 \x01(\x03\x12\x10\n\x08operator\x18\x05 \x01(\t\"S\n\x10RollbackResponse\x12\x12\n\nrelease_id\x18\x01 \x01(\x03\x12\x1b\n\x13new_release_version\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t2\xb7\x01\n\rConfigService\x12R\n\tGetConfig\x12!.configcenter.v1.GetConfigRequest\x1a\".configcenter.v1.GetConfigResponse\x12R\n\x0bWatchConfig\x12#.configcenter.v1.WatchConfigRequest\x1a\x1c.configcenter.v1.ConfigEvent0\x01\x32\xb9\x01\n\x0c\x41\x64minService\x12R\n\rPublishConfig\x12\x1f.configcenter.v1.PublishRequest\x1a .configcenter.v1.PublishResponse\x12U\n\x0eRollbackConfig\x12 .configcenter.v1.RollbackRequest\x1a!.configcenter.v1.RollbackResponseB>Z<github.com/longpeng/configcenter/pkg/proto/v1;configcenterv1b\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'configcenter.v1.config_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
_globals['DESCRIPTOR']._loaded_options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'Z<github.com/longpeng/configcenter/pkg/proto/v1;configcenterv1'
|
||||
_globals['_CONFIGITEM']._serialized_start=49
|
||||
_globals['_CONFIGITEM']._serialized_end=89
|
||||
_globals['_GETCONFIGREQUEST']._serialized_start=91
|
||||
_globals['_GETCONFIGREQUEST']._serialized_end=184
|
||||
_globals['_GETCONFIGRESPONSE']._serialized_start=186
|
||||
_globals['_GETCONFIGRESPONSE']._serialized_end=292
|
||||
_globals['_WATCHCONFIGREQUEST']._serialized_start=294
|
||||
_globals['_WATCHCONFIGREQUEST']._serialized_end=413
|
||||
_globals['_CONFIGEVENT']._serialized_start=416
|
||||
_globals['_CONFIGEVENT']._serialized_end=586
|
||||
_globals['_CONFIGEVENT_EVENTTYPE']._serialized_start=547
|
||||
_globals['_CONFIGEVENT_EVENTTYPE']._serialized_end=586
|
||||
_globals['_PUBLISHREQUEST']._serialized_start=588
|
||||
_globals['_PUBLISHREQUEST']._serialized_end=693
|
||||
_globals['_PUBLISHRESPONSE']._serialized_start=695
|
||||
_globals['_PUBLISHRESPONSE']._serialized_end=773
|
||||
_globals['_ROLLBACKREQUEST']._serialized_start=775
|
||||
_globals['_ROLLBACKREQUEST']._serialized_end=888
|
||||
_globals['_ROLLBACKRESPONSE']._serialized_start=890
|
||||
_globals['_ROLLBACKRESPONSE']._serialized_end=973
|
||||
_globals['_CONFIGSERVICE']._serialized_start=976
|
||||
_globals['_CONFIGSERVICE']._serialized_end=1159
|
||||
_globals['_ADMINSERVICE']._serialized_start=1162
|
||||
_globals['_ADMINSERVICE']._serialized_end=1347
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
255
sdk/python/configcenter/v1/config_pb2_grpc.py
Normal file
255
sdk/python/configcenter/v1/config_pb2_grpc.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
from configcenter.v1 import config_pb2 as configcenter_dot_v1_dot_config__pb2
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.71.0'
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ f' but the generated code in configcenter/v1/config_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
)
|
||||
|
||||
|
||||
class ConfigServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.GetConfig = channel.unary_unary(
|
||||
'/configcenter.v1.ConfigService/GetConfig',
|
||||
request_serializer=configcenter_dot_v1_dot_config__pb2.GetConfigRequest.SerializeToString,
|
||||
response_deserializer=configcenter_dot_v1_dot_config__pb2.GetConfigResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.WatchConfig = channel.unary_stream(
|
||||
'/configcenter.v1.ConfigService/WatchConfig',
|
||||
request_serializer=configcenter_dot_v1_dot_config__pb2.WatchConfigRequest.SerializeToString,
|
||||
response_deserializer=configcenter_dot_v1_dot_config__pb2.ConfigEvent.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class ConfigServiceServicer(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def GetConfig(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def WatchConfig(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_ConfigServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'GetConfig': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetConfig,
|
||||
request_deserializer=configcenter_dot_v1_dot_config__pb2.GetConfigRequest.FromString,
|
||||
response_serializer=configcenter_dot_v1_dot_config__pb2.GetConfigResponse.SerializeToString,
|
||||
),
|
||||
'WatchConfig': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.WatchConfig,
|
||||
request_deserializer=configcenter_dot_v1_dot_config__pb2.WatchConfigRequest.FromString,
|
||||
response_serializer=configcenter_dot_v1_dot_config__pb2.ConfigEvent.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'configcenter.v1.ConfigService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('configcenter.v1.ConfigService', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class ConfigService(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def GetConfig(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/configcenter.v1.ConfigService/GetConfig',
|
||||
configcenter_dot_v1_dot_config__pb2.GetConfigRequest.SerializeToString,
|
||||
configcenter_dot_v1_dot_config__pb2.GetConfigResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def WatchConfig(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/configcenter.v1.ConfigService/WatchConfig',
|
||||
configcenter_dot_v1_dot_config__pb2.WatchConfigRequest.SerializeToString,
|
||||
configcenter_dot_v1_dot_config__pb2.ConfigEvent.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class AdminServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.PublishConfig = channel.unary_unary(
|
||||
'/configcenter.v1.AdminService/PublishConfig',
|
||||
request_serializer=configcenter_dot_v1_dot_config__pb2.PublishRequest.SerializeToString,
|
||||
response_deserializer=configcenter_dot_v1_dot_config__pb2.PublishResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.RollbackConfig = channel.unary_unary(
|
||||
'/configcenter.v1.AdminService/RollbackConfig',
|
||||
request_serializer=configcenter_dot_v1_dot_config__pb2.RollbackRequest.SerializeToString,
|
||||
response_deserializer=configcenter_dot_v1_dot_config__pb2.RollbackResponse.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class AdminServiceServicer(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def PublishConfig(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def RollbackConfig(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_AdminServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'PublishConfig': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.PublishConfig,
|
||||
request_deserializer=configcenter_dot_v1_dot_config__pb2.PublishRequest.FromString,
|
||||
response_serializer=configcenter_dot_v1_dot_config__pb2.PublishResponse.SerializeToString,
|
||||
),
|
||||
'RollbackConfig': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.RollbackConfig,
|
||||
request_deserializer=configcenter_dot_v1_dot_config__pb2.RollbackRequest.FromString,
|
||||
response_serializer=configcenter_dot_v1_dot_config__pb2.RollbackResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'configcenter.v1.AdminService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('configcenter.v1.AdminService', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class AdminService(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def PublishConfig(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/configcenter.v1.AdminService/PublishConfig',
|
||||
configcenter_dot_v1_dot_config__pb2.PublishRequest.SerializeToString,
|
||||
configcenter_dot_v1_dot_config__pb2.PublishResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def RollbackConfig(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/configcenter.v1.AdminService/RollbackConfig',
|
||||
configcenter_dot_v1_dot_config__pb2.RollbackRequest.SerializeToString,
|
||||
configcenter_dot_v1_dot_config__pb2.RollbackResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
@@ -8,6 +8,12 @@ version = "0.1.0"
|
||||
description = "Config Center Python client with SSE watch and local cache fallback"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[project.optional-dependencies]
|
||||
grpc = [
|
||||
"grpcio>=1.71,<2",
|
||||
"protobuf>=5.29,<7",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["configcenter*"]
|
||||
|
||||
Reference in New Issue
Block a user